From 05bcba88c0f6d7c852f7dc14dc09b9fc3cc3693a Mon Sep 17 00:00:00 2001 From: suja-envitia Date: Thu, 1 Feb 2024 10:41:40 +0000 Subject: [PATCH 1/2] Added temperature grid, depth profile and vertical slice views --- .../DrawLineInteractionMode.cs | 201 ++++++++ .../DrawingSurfacePanel.csproj | 1 + DrawingSurfacePanel/MapViewerPanel.cs | 254 +++++++--- .../Ascii/AsciiGridDataset.cs | 231 +++++++++ .../Ascii/AsciiGridHeader.cs | 41 ++ Envitia.MapLink.Grids/ColourScales.cs | 91 ++++ Envitia.MapLink.Grids/Cube.cs | 40 ++ Envitia.MapLink.Grids/DataGrid.cs | 347 +++++++++++++ Envitia.MapLink.Grids/DepthGrid.cs | 122 +++++ .../Envitia.MapLink.Grids.csproj | 77 +++ Envitia.MapLink.Grids/Filter.cs | 21 + Envitia.MapLink.Grids/GridDataset.cs | 55 ++ Envitia.MapLink.Grids/GridLayer.cs | 235 +++++++++ Envitia.MapLink.Grids/Line.cs | 43 ++ .../Properties/AssemblyInfo.cs | 36 ++ Envitia.MapLink.Grids/RadialGridLayer.cs | 149 ++++++ MapLinkProApp/App.config | 2 + MapLinkProApp/App.xaml | 132 ++++- MapLinkProApp/ColourScales.xml | 11 + MapLinkProApp/CrossSectionPanel.cs | 276 ++++++++++ MapLinkProApp/DepthProfile.xaml | 63 +++ MapLinkProApp/DepthProfile.xaml.cs | 89 ++++ MapLinkProApp/DepthProfilePanel.cs | 83 +++ MapLinkProApp/DockManager.cs | 209 ++++++++ MapLinkProApp/DockableWindow.cs | 56 +++ MapLinkProApp/LayerProperty.cs | 25 + MapLinkProApp/LayerSelector.cs | 476 ++++++++++++++++++ MapLinkProApp/MainWindow.xaml | 68 ++- MapLinkProApp/MainWindow.xaml.cs | 406 ++++++--------- MapLinkProApp/MapLayers.xml | 407 ++++++++++++++- MapLinkProApp/MapLayers/AsciiGridMapLayer.cs | 63 +++ MapLinkProApp/MapLayers/MapLayer.cs | 16 +- MapLinkProApp/MapLayers/MapLinkMapLayer.cs | 28 +- MapLinkProApp/MapLayers/NativeMapLayer.cs | 17 + MapLinkProApp/MapLinkProApp.csproj | 45 ++ MapLinkProApp/MapLinkProApp.sln | 27 + MapLinkProApp/Maps.cs | 122 ++++- MapLinkProApp/Profile.cs | 114 +++++ MapLinkProApp/SliderProperty.cs | 21 + MapLinkProApp/SliderTickBarWithLabel.cs | 53 ++ MapLinkProApp/SliderWithCustomToolTip.cs | 75 +++ MapLinkProApp/VerticalCrossSection.cs | 47 ++ MapLinkProApp/VerticalSlice.xaml | 63 +++ MapLinkProApp/VerticalSlice.xaml.cs | 90 ++++ ..._anfc_0,083deg_P1D-m_1702458667101_0,5.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_1,5.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_1062,4.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_109,7.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_11,4.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_1245,3.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_13,5.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_130,7.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_1452,3.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_15,8.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_155,9.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_1684,3.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_18,5.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_186,1.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_1941,9.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_2,6.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_21,6.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_222,5.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_2225,1.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_25,2.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_2533,3.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_266,0.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_2865,7.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_29,4.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_3,8.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_318,1.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_3220,8.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_34,4.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_3597,0.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_380,2.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_3992,5.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_40,3.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_4405,2.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_453,9.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_47,4.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_4833,3.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_5,1.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_5274,8.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_541,1.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_55,8.asc | 126 +++++ ...fc_0,083deg_P1D-m_1702458667101_5727,9.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_6,4.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_643,6.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_65,8.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_7,9.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_763,3.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_77,9.asc | 126 +++++ ..._anfc_0,083deg_P1D-m_1702458667101_9,6.asc | 126 +++++ ...nfc_0,083deg_P1D-m_1702458667101_902,3.asc | 126 +++++ ...anfc_0,083deg_P1D-m_1702458667101_92,3.asc | 126 +++++ MapLinkProApp/img/dock-vert.png | Bin 0 -> 7259 bytes MapLinkProApp/img/max.png | Bin 0 -> 9046 bytes MapLinkProApp/img/min-right.png | Bin 0 -> 5414 bytes MapLinkProApp/soundspeed.ctr | 98 ++++ MapLinkProApp/temperature.ctr | 177 +++++++ 99 files changed, 11273 insertions(+), 330 deletions(-) create mode 100644 DrawingSurfacePanel/DrawLineInteractionMode.cs create mode 100644 Envitia.MapLink.Grids/Ascii/AsciiGridDataset.cs create mode 100644 Envitia.MapLink.Grids/Ascii/AsciiGridHeader.cs create mode 100644 Envitia.MapLink.Grids/ColourScales.cs create mode 100644 Envitia.MapLink.Grids/Cube.cs create mode 100644 Envitia.MapLink.Grids/DataGrid.cs create mode 100644 Envitia.MapLink.Grids/DepthGrid.cs create mode 100644 Envitia.MapLink.Grids/Envitia.MapLink.Grids.csproj create mode 100644 Envitia.MapLink.Grids/Filter.cs create mode 100644 Envitia.MapLink.Grids/GridDataset.cs create mode 100644 Envitia.MapLink.Grids/GridLayer.cs create mode 100644 Envitia.MapLink.Grids/Line.cs create mode 100644 Envitia.MapLink.Grids/Properties/AssemblyInfo.cs create mode 100644 Envitia.MapLink.Grids/RadialGridLayer.cs create mode 100644 MapLinkProApp/ColourScales.xml create mode 100644 MapLinkProApp/CrossSectionPanel.cs create mode 100644 MapLinkProApp/DepthProfile.xaml create mode 100644 MapLinkProApp/DepthProfile.xaml.cs create mode 100644 MapLinkProApp/DepthProfilePanel.cs create mode 100644 MapLinkProApp/DockManager.cs create mode 100644 MapLinkProApp/DockableWindow.cs create mode 100644 MapLinkProApp/LayerProperty.cs create mode 100644 MapLinkProApp/LayerSelector.cs create mode 100644 MapLinkProApp/MapLayers/AsciiGridMapLayer.cs create mode 100644 MapLinkProApp/Profile.cs create mode 100644 MapLinkProApp/SliderProperty.cs create mode 100644 MapLinkProApp/SliderTickBarWithLabel.cs create mode 100644 MapLinkProApp/SliderWithCustomToolTip.cs create mode 100644 MapLinkProApp/VerticalCrossSection.cs create mode 100644 MapLinkProApp/VerticalSlice.xaml create mode 100644 MapLinkProApp/VerticalSlice.xaml.cs create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_0,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1062,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_109,7.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_11,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1245,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_13,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_130,7.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1452,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_15,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_155,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1684,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_18,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_186,1.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_1941,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_2,6.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_21,6.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_222,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_2225,1.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_25,2.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_2533,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_266,0.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_2865,7.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_29,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_3,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_318,1.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_3220,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_34,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_3597,0.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_380,2.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_3992,5.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_40,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_4405,2.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_453,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_47,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_4833,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_5,1.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_5274,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_541,1.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_55,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_5727,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_6,4.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_643,6.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_65,8.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_7,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_763,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_77,9.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_9,6.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_902,3.asc create mode 100644 MapLinkProApp/griddata/temperature/ASCII/cmems_mod_glo_phy-thetao_anfc_0,083deg_P1D-m_1702458667101_92,3.asc create mode 100644 MapLinkProApp/img/dock-vert.png create mode 100644 MapLinkProApp/img/max.png create mode 100644 MapLinkProApp/img/min-right.png create mode 100644 MapLinkProApp/soundspeed.ctr create mode 100644 MapLinkProApp/temperature.ctr diff --git a/DrawingSurfacePanel/DrawLineInteractionMode.cs b/DrawingSurfacePanel/DrawLineInteractionMode.cs new file mode 100644 index 0000000..a93702b --- /dev/null +++ b/DrawingSurfacePanel/DrawLineInteractionMode.cs @@ -0,0 +1,201 @@ +using System; +using Envitia.MapLink; +using Envitia.MapLink.InteractionModes; +using Envitia.MapLink.OpenGLSurface; + +namespace DrawingSurfacePanel +{ + /// + /// This class demonstrates how to implement a custom interaction mode + /// + public class DrawLineInterationMode : TSLNInteractionMode + { + public interface IObserver + { + void Invalidate(); + void NewLine(Tuple startMu, Tuple endMu); + } + public System.Collections.Generic.List Observers { get; } = new System.Collections.Generic.List(); + + public int ID { get; } + + public TSLNCoord StartCoord { get; set; } + public TSLNCoord EndCoord { get; set; } + + public bool IsActive { get; private set; } + + public TSLNStandardDataLayer Overlay { get; } = new TSLNStandardDataLayer(); + + private TSLNPolyline Line { get; set; } + + enum FeatureIDs + { + FeatureStartEllipse = 6786876, + FeatureEndEllipse, + FeatureLine + } + + public DrawLineInterationMode(int id) + : base(id, false) + { + ID = id; + } + + private static void SetSymbolRendering(TSLNEntity symbol, bool isStart) + { + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeSymbolStyle, 3); + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeSymbolColour, isStart ? 19 : 1); + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeSymbolMinPixelSize, 20); + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeSymbolMaxPixelSize, 20); + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeDouble.TSLNRenderingAttributeEdgeThickness, 3); + symbol.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeEdgeColour, System.Drawing.Color.FromArgb(150, 255, 255, 255).ToArgb()); + } + + + private static void SetLineRendering(Envitia.MapLink.TSLNEntity line) + { + if (line != null) + { + line.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeEdgeStyle, 7); + line.setRendering(Envitia.MapLink.TSLNRenderingAttributeDouble.TSLNRenderingAttributeEdgeThickness, 1); + line.setRendering(Envitia.MapLink.TSLNRenderingAttributeInt.TSLNRenderingAttributeEdgeColour, System.Drawing.Color.FromArgb(255, 0, 0, 0).ToArgb()); + } + } + + private TSLNSymbol AddSymbol(int featureID, TSLNCoord location) + { + var symbol = Overlay.entitySet.createSymbol(featureID, location.x, location.y); + SetSymbolRendering(symbol, true); + return symbol; + } + + private TSLNPolyline AddLine(TSLNCoord start, TSLNCoord end) + { + var lineCoords = new TSLNCoordSet(); + lineCoords.add(start); + lineCoords.add(end); + var line = Overlay.entitySet.createPolyline((int)FeatureIDs.FeatureLine, lineCoords); + SetLineRendering(line); + + return line; + } + + public void Initialise() + { + AddSymbol((int)FeatureIDs.FeatureStartEllipse, StartCoord); + AddSymbol((int)FeatureIDs.FeatureEndEllipse, EndCoord); + AddLine(StartCoord, EndCoord); + } + + public override bool onLButtonDown(int x, int y, bool shift, bool control) + { + if (this.display != null && this.display.drawingSurfaceBase != null) + { + IsActive = true; + + var ds = (TSLNDrawingSurface)this.display.drawingSurfaceBase; + int tmcX = 0; + int tmcY = 0; + ds.DUToTMC(x, y, out tmcX, out tmcY); + + StartCoord = new TSLNCoord( tmcX, tmcY); + + Overlay.entitySet.clear(); + + AddSymbol((int)FeatureIDs.FeatureStartEllipse, StartCoord); + Line = AddLine(StartCoord, new TSLNCoord(StartCoord.x + 50, StartCoord.y + 50)); + + Overlay.notifyChanged(); + + this.display.viewChanged(true); + display.drawingSurface.redraw(); + + foreach (var observer in Observers) + { + observer.Invalidate(); + } + + return true; + } + return false; + } + + public override bool onMouseMove(TSLNButtonType button, int x, int y, bool shift, bool control) + { + if (!IsActive) + return base.onMouseMove(button, x, y, shift, control); + + var ds = (TSLNDrawingSurface)this.display.drawingSurfaceBase; + int tmcX = 0; + int tmcY = 0; + ds.DUToTMC(x, y, out tmcX, out tmcY); + + var lineCoords = new TSLNCoordSet(); + lineCoords.add(StartCoord); + lineCoords.add(tmcX, tmcY); + Line.points(lineCoords); + + Overlay.notifyChanged(); + this.display.viewChanged(true); + + foreach (var observer in Observers) + { + observer.Invalidate(); + } + + return true; + } + + public override bool onLButtonUp(int x, int y, bool shift, bool control) + { + if (!IsActive) + return base.onLButtonUp(x, y, shift, control); + + IsActive = false; + + var ds = (TSLNDrawingSurface)this.display.drawingSurfaceBase; + int tmcX = 0; + int tmcY = 0; + ds.DUToTMC(x, y, out tmcX, out tmcY); + EndCoord = new TSLNCoord(tmcX, tmcY); + + AddSymbol((int)FeatureIDs.FeatureEndEllipse, EndCoord); + + var lineCoords = new TSLNCoordSet(); + lineCoords.add(StartCoord); + lineCoords.add(tmcX, tmcY); + Line.points(lineCoords); + + double startMuX = 0; + double startMuY = 0; + ds.TMCToMU(StartCoord.x, StartCoord.y, out startMuX, out startMuY); + var start = new Tuple(startMuX, startMuY); + double endMuX = 0; + double endMuY = 0; + ds.TMCToMU(EndCoord.x, EndCoord.y, out endMuX, out endMuY); + var end = new Tuple(endMuX, endMuY); + + Overlay.notifyChanged(); + this.display.viewChanged(true); + + foreach (var observer in Observers) + { + observer.Invalidate(); + observer.NewLine(start, end); + } + + return true; + } + + public override Envitia.MapLink.TSLNCursorStyle queryCursor() + { + return TSLNCursorStyle.TSLNCursorStyleMovePoint; + } + + public override string queryPrompt() + { + return "Draw a line to select depth slice"; + } + + } +} diff --git a/DrawingSurfacePanel/DrawingSurfacePanel.csproj b/DrawingSurfacePanel/DrawingSurfacePanel.csproj index 75e3023..d035206 100644 --- a/DrawingSurfacePanel/DrawingSurfacePanel.csproj +++ b/DrawingSurfacePanel/DrawingSurfacePanel.csproj @@ -128,6 +128,7 @@ Code + Component diff --git a/DrawingSurfacePanel/MapViewerPanel.cs b/DrawingSurfacePanel/MapViewerPanel.cs index 586bb4e..d4286ee 100644 --- a/DrawingSurfacePanel/MapViewerPanel.cs +++ b/DrawingSurfacePanel/MapViewerPanel.cs @@ -23,7 +23,7 @@ namespace DrawingSurfacePanel /// /// Summary description for drawingSurfacePanel. /// - public class MapViewerPanel : Panel, IPanel + public class MapViewerPanel : Panel, IPanel, DrawLineInterationMode.IObserver { public IPanel GetIPanel() { return this; } @@ -37,7 +37,7 @@ public enum InteractionModeEnum TOOLS_DRAW_LINE } - struct contextMenuTool + struct ContextMenuTool { public TSLNInteractionMode interactionMode; public InteractionModeEnum mode; @@ -45,15 +45,18 @@ struct contextMenuTool public bool ischecked; } - contextMenuTool[] m_contextMenuTools; + private ContextMenuTool[] ContextMenuTools { get; set; } - private InteractionModeEnum m_currentInteractionMode; + private InteractionModeEnum CurrentInteractionMode { get; set; } //Create out request receiver pointer - private InteractionModeRequestReceiver m_updateReceiver = null; + private InteractionModeRequestReceiver UpdateReceiver { get; set; } = null; //Create interaction modes' pointers - private TSLNInteractionModeManagerGeneric m_modeManager = null; + private TSLNInteractionModeManagerGeneric ModeManager { get; set; } = null; + + // The interaction mode to draw a line on the map. + public DrawLineInterationMode DrawLineInterationMode { get; set; } = new DrawLineInterationMode((int)InteractionModeEnum.TOOLS_DRAW_LINE); public TSLNStandardDataLayer GeometryLayer { get; } = new TSLNStandardDataLayer(); @@ -62,13 +65,19 @@ struct contextMenuTool //Create the usual MapLink Item pointers public DrawingSurfaceClass DrawingSurface { get; set; } + // context menu when right click is clicked + ContextMenuStrip ContextMenu { get; set; } = null; + + public event rightClickCurrentModeChangeHandler RightClickCurrentModeChange; + public delegate void rightClickCurrentModeChangeHandler(); + #endregion #region Panel_Contructors public MapViewerPanel() { - initializeComponent(); + InitializeComponent(); //! Set Maplink Home directory here ... string maplHome = TSLNUtilityFunctions.getMapLinkHome(); if (string.IsNullOrEmpty(maplHome)) @@ -76,6 +85,8 @@ public MapViewerPanel() TSLNUtilityFunctions.setMapLinkHome("../"); } + DrawLineInterationMode.Observers.Add(this); + //! Initialise drawing surface Envitia.MapLink.TSLNDrawingSurface.loadStandardConfig(); @@ -84,7 +95,7 @@ public MapViewerPanel() ~MapViewerPanel() { - cleanUp(); + CleanUp(); } #endregion @@ -92,7 +103,7 @@ public MapViewerPanel() private MapViewerParentPanel ViewerPanel; - private void initializeComponent() + private void InitializeComponent() { //! define the panel's controls this.ViewerPanel = new MapViewerParentPanel(); @@ -134,9 +145,9 @@ private void OnPaint(object sender, System.Windows.Forms.PaintEventArgs e) } // Don't forget to draw any echo rectangle that may be active. - if (m_modeManager != null) + if (ModeManager != null) { - m_modeManager.onDraw(e.ClipRectangle.Left + leftOffset, e.ClipRectangle.Top, e.ClipRectangle.Right, e.ClipRectangle.Bottom - bottomOffset); + ModeManager.onDraw(e.ClipRectangle.Left + leftOffset, e.ClipRectangle.Top, e.ClipRectangle.Right, e.ClipRectangle.Bottom - bottomOffset); } } @@ -147,9 +158,9 @@ private void OnReSize(object sender, System.EventArgs e) { DrawingSurface.wndResize(ViewerPanel.DisplayRectangle.Left, ViewerPanel.DisplayRectangle.Top, ViewerPanel.DisplayRectangle.Right, ViewerPanel.DisplayRectangle.Bottom, false, TSLNResizeActionEnum.TSLNResizeActionMaintainCentre); } - if (m_modeManager != null) + if (ModeManager != null) { - m_modeManager.onSize(ViewerPanel.DisplayRectangle.Width, ViewerPanel.DisplayRectangle.Height); + ModeManager.onSize(ViewerPanel.DisplayRectangle.Width, ViewerPanel.DisplayRectangle.Height); } // Without the invalidate, the paint is only called when the window gets bigger @@ -159,7 +170,7 @@ private void OnReSize(object sender, System.EventArgs e) //On mouse down on the ViewerPanel private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { - if (m_modeManager == null) + if (ModeManager == null) { return; } @@ -170,17 +181,17 @@ private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { case MouseButtons.Left: { - invalidate = m_modeManager.onLButtonDown(e.X, e.Y, false, false); + invalidate = ModeManager.onLButtonDown(e.X, e.Y, false, false); break; } case MouseButtons.Middle: { - invalidate = m_modeManager.onMButtonDown(e.X, e.Y, false, false); + invalidate = ModeManager.onMButtonDown(e.X, e.Y, false, false); break; } case MouseButtons.Right: { - invalidate = m_modeManager.onRButtonDown(e.X, e.Y, false, false); + invalidate = ModeManager.onRButtonDown(e.X, e.Y, false, false); break; } } @@ -192,7 +203,7 @@ private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e) //On mouse move on the ViewerPanel private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e) { - if (m_modeManager == null) + if (ModeManager == null) { return; } @@ -205,7 +216,7 @@ private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e) else if (e.Button == MouseButtons.Right) button = TSLNButtonType.TSLNButtonRight; - if (m_modeManager.onMouseMove(button, e.X, e.Y, false, false)) + if (ModeManager.onMouseMove(button, e.X, e.Y, false, false)) { // Request a redraw if the interaction hander requires it ViewerPanel.Invalidate(); @@ -215,7 +226,7 @@ private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e) //On mouse up on the ViewerPanel private void OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e) { - if (m_modeManager == null) + if (ModeManager == null) { return; } @@ -225,16 +236,17 @@ private void OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e) { case MouseButtons.Left: { - invalidate = m_modeManager.onLButtonUp(e.X, e.Y, false, false); + invalidate = ModeManager.onLButtonUp(e.X, e.Y, false, false); break; } case MouseButtons.Middle: { - invalidate = m_modeManager.onMButtonUp(e.X, e.Y, false, false); + invalidate = ModeManager.onMButtonUp(e.X, e.Y, false, false); break; } case MouseButtons.Right: { + ShowRClickContextMenu(e); break; } } @@ -248,11 +260,11 @@ private void OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e) //On mouse wheel on the ViewerPanel private void OnMouseWheel(object sender, System.Windows.Forms.MouseEventArgs e) { - if (m_modeManager == null) + if (ModeManager == null) { return; } - if (m_modeManager.onMouseWheel((short)e.Delta, e.X, e.Y)) + if (ModeManager.onMouseWheel((short)e.Delta, e.X, e.Y)) { ViewerPanel.Invalidate(); } @@ -260,7 +272,92 @@ private void OnMouseWheel(object sender, System.Windows.Forms.MouseEventArgs e) private void OnDestroyed(object sender, System.EventArgs e) { - cleanUp(); + CleanUp(); + } + + /// + /// initialize the context menu by setting the options + /// + private void InitializeRClickContextMenu() + { + if (ContextMenu != null) + return; + + ContextMenu = new ContextMenuStrip(); + for (int idx = 0; idx < ContextMenuTools.Length; ++idx) + { + ContextMenu.Items.Add(ContextMenuTools[idx].text); + ((ToolStripMenuItem)ContextMenu.Items[idx]).Checked = ContextMenuTools[idx].ischecked; + } + } + + /// + /// when a check menu item is chosen, it unchecks other options + /// + /// + /// + /// + private void UpdateContextMenuToolChecked(InteractionModeEnum mode, ContextMenuStrip contexMenuuu, ContextMenuTool[] tools) + { + if (contexMenuuu == null) + return; + + for (int idx = 0; idx < ContextMenuTools.Length; ++idx) + { + ((ToolStripMenuItem)contexMenuuu.Items[idx]).Checked = ContextMenuTools[idx].mode == mode; + } + } + + private void UpdateRightClickModeChangeEvent() + { + if (this.RightClickCurrentModeChange != null) + { + this.RightClickCurrentModeChange(); + } + } + + /// + /// show right click context menu + /// + /// + private void ShowRClickContextMenu(System.Windows.Forms.MouseEventArgs e) + { + if (ContextMenu == null) + return; + + ContextMenu.Show(PointToScreen(e.Location)); + ContextMenu.ItemClicked += new ToolStripItemClickedEventHandler( + ContexMenuuu_ItemClicked); + } + + /// + /// handles when an option is chosen from the right click context menu. + /// + /// + /// + private void ContexMenuuu_ItemClicked(object sender, ToolStripItemClickedEventArgs e) + { + ToolStripItem item = e.ClickedItem; + // your code here + switch (item.Text) + { + case "Grab Tool": + if (CurrentInteractionMode != InteractionModeEnum.TOOLS_GRAB) + { + SetCurrentMode(InteractionModeEnum.TOOLS_GRAB); + UpdateRightClickModeChangeEvent(); + } + break; + + case "Draw Slice": + if (CurrentInteractionMode != InteractionModeEnum.TOOLS_DRAW_LINE) + { + SetCurrentMode(InteractionModeEnum.TOOLS_DRAW_LINE); + UpdateRightClickModeChangeEvent(); + } + break; + } + } #endregion @@ -280,17 +377,17 @@ public void InitializePanel() DrawingSurface.setOption(TSLNOptionEnum.TSLNOptionAntiAliasMonoRasters, true); #endif - m_updateReceiver = new InteractionModeRequestReceiver(); + UpdateReceiver = new InteractionModeRequestReceiver(); // Attach the drawing surface to the form and set it's initial size... // This must be done before the drawingsurface is given to the // InteractionModeManager DrawingSurface.attach(ViewerPanel.Handle, false); // Create our user defined request receiver - TSLNInteractionModeRequest request = (TSLNInteractionModeRequest)m_updateReceiver; + TSLNInteractionModeRequest request = (TSLNInteractionModeRequest)UpdateReceiver; // Now create the mode manager - m_modeManager = new TSLNInteractionModeManagerGeneric(request, DrawingSurface, 5, 5, 30, true); + ModeManager = new TSLNInteractionModeManagerGeneric(request, DrawingSurface, 5, 5, 30, true); // Next create our modes InitializeInteractionModes(); @@ -298,18 +395,20 @@ public void InitializePanel() //Give them to the interaction mode manager //Remember we don't own the modes anymore after this point //but we must keep a reference to them or c# won't deleted them properly - foreach (var tool in m_contextMenuTools) + foreach (var tool in ContextMenuTools) { - m_modeManager.addMode(tool.interactionMode, tool.ischecked); + ModeManager.addMode(tool.interactionMode, tool.ischecked); } DrawingSurface.wndResize(ViewerPanel.DisplayRectangle.Left, ViewerPanel.DisplayRectangle.Top, ViewerPanel.DisplayRectangle.Right, ViewerPanel.DisplayRectangle.Bottom, false, TSLNResizeActionEnum.TSLNResizeActionMaintainCentre); - m_modeManager.onSize(ViewerPanel.DisplayRectangle.Width, ViewerPanel.DisplayRectangle.Height); + ModeManager.onSize(ViewerPanel.DisplayRectangle.Width, ViewerPanel.DisplayRectangle.Height); DrawingSurface.setOption(TSLNOptionEnum.TSLNOptionDoubleBuffered, true); //activate grab imode - setCurrentMode(InteractionModeEnum.TOOLS_GRAB); + SetCurrentMode(InteractionModeEnum.TOOLS_GRAB); + + InitializeRClickContextMenu(); DrawingSurface.reset(false); } @@ -358,27 +457,32 @@ private void EnsureVisible(Envitia.MapLink.TSLNDataLayer dataLayer, string id) public void EnsureInteractionLayersVisible() { EnsureVisible(GeometryLayer, "Geometry"); + EnsureVisible(DrawLineInterationMode.Overlay, "DrawLineInterationMode.Overlay"); } private void InitializeInteractionModes() { - m_contextMenuTools = new contextMenuTool[] + ContextMenuTools = new ContextMenuTool[] { - new contextMenuTool { mode = InteractionModeEnum.TOOLS_GRAB, text = "Grab Tool", ischecked = false}, + new ContextMenuTool { mode = InteractionModeEnum.TOOLS_GRAB, text = "Grab Tool", ischecked = false}, + new ContextMenuTool { mode = InteractionModeEnum.TOOLS_DRAW_LINE, text = "Draw Slice", ischecked = false} }; - for (int i = 0; i < m_contextMenuTools.Length; ++i) + for (int i = 0; i < ContextMenuTools.Length; ++i) { - switch (m_contextMenuTools[i].text) + switch (ContextMenuTools[i].text) { case "Grab Tool": - m_contextMenuTools[i].interactionMode = new TSLNInteractionModeGrab((int)InteractionModeEnum.TOOLS_GRAB, true, "Left button drag move view, Right button click to finish", true); + ContextMenuTools[i].interactionMode = new TSLNInteractionModeGrab((int)InteractionModeEnum.TOOLS_GRAB, true, "Left button drag move view, Right button click to finish", true); + break; + case "Draw Slice": + ContextMenuTools[i].interactionMode = DrawLineInterationMode; break; } } } - private void cleanUp() + private void CleanUp() { DrawingSurfaceClass.cleanup(); DrawingSurface.Dispose(); @@ -394,62 +498,83 @@ private static extern bool #region Panel_ZoomPan_Functions - public bool setCurrentMode(InteractionModeEnum mode) + public bool SetCurrentMode(InteractionModeEnum mode) { - if (m_modeManager == null) + if (ModeManager == null) { return false; } bool invalidate = false; - for (int i = 0; i < m_contextMenuTools.Length; ++i) + for (int i = 0; i < ContextMenuTools.Length; ++i) { - if (mode == m_contextMenuTools[i].mode) + if (mode == ContextMenuTools[i].mode) { - invalidate = m_modeManager.setCurrentMode((int)mode); + invalidate = ModeManager.setCurrentMode((int)mode); break; } } if (invalidate) { - m_currentInteractionMode = mode; + CurrentInteractionMode = mode; + UpdateContextMenuToolChecked(mode, ContextMenu, ContextMenuTools); ViewerPanel.Invalidate(); } return invalidate; } + public InteractionModeEnum GetCurrentMode() + { + if (ModeManager == null) + { + return InteractionModeEnum.InValid; + } + + TSLNInteractionMode cmode = null; + long id = ModeManager.getCurrentMode(out cmode); + foreach (var tool in ContextMenuTools) + { + if (id == (int)tool.mode) + { + return tool.mode; + } + } + + return InteractionModeEnum.InValid; + } + #endregion #region Panel_StackViews_Functions - public bool saveView(int index) + public bool SaveView(int index) { - if (m_modeManager == null) + if (ModeManager == null) { return false; } - bool invalidate = m_modeManager.savedViewSetToCurrent(index); + bool invalidate = ModeManager.savedViewSetToCurrent(index); return invalidate; } - public bool viewStackReset() + public bool ViewStackReset() { - if (m_modeManager == null) + if (ModeManager == null) { return false; } - bool invalidate = m_modeManager.savedViewReset(); + bool invalidate = ModeManager.savedViewReset(); return invalidate; } - public bool gotoSavedView(int index) + public bool GotoSavedView(int index) { - if (m_modeManager == null) + if (ModeManager == null) { return false; } - bool invalidate = m_modeManager.savedViewGoto(index); + bool invalidate = ModeManager.savedViewGoto(index); if (invalidate) { ViewerPanel.Invalidate(); @@ -457,13 +582,13 @@ public bool gotoSavedView(int index) return invalidate; } - public bool viewStackGotoPrevious() + public bool ViewStackGotoPrevious() { - if (m_modeManager == null) + if (ModeManager == null) { return false; } - bool invalidate = m_modeManager.viewStackGotoPrevious(); + bool invalidate = ModeManager.viewStackGotoPrevious(); if (invalidate) { ViewerPanel.Invalidate(); @@ -471,13 +596,13 @@ public bool viewStackGotoPrevious() return invalidate; } - public bool viewStackGotoNext() + public bool ViewStackGotoNext() { - if (m_modeManager == null) + if (ModeManager == null) { return false; } - bool invalidate = m_modeManager.viewStackGotoNext(); + bool invalidate = ModeManager.viewStackGotoNext(); if (invalidate) { ViewerPanel.Invalidate(); @@ -485,6 +610,17 @@ public bool viewStackGotoNext() return invalidate; } + void DrawLineInterationMode.IObserver.Invalidate() + { + ViewerPanel.Invalidate(); + base.Invalidate(); + } + + void DrawLineInterationMode.IObserver.NewLine(Tuple startMu, Tuple endMu) + { + EnsureInteractionLayersVisible(); + } + #endregion public void SafeInvalidate() diff --git a/Envitia.MapLink.Grids/Ascii/AsciiGridDataset.cs b/Envitia.MapLink.Grids/Ascii/AsciiGridDataset.cs new file mode 100644 index 0000000..64b89d7 --- /dev/null +++ b/Envitia.MapLink.Grids/Ascii/AsciiGridDataset.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Threading.Tasks; + +namespace Envitia.MapLink.Grids.Ascii +{ + public class AsciiGridDataset : GridDataset + { + private const int PIXEL_OFFSET = 0; + + AsciiGridHeader header; + + /// + /// Persists the ASCII header values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private AsciiGridHeader populateHeader(int noX, int noY, double blX, double blY, double noDataVal, double cellSize, bool isBLLx, bool isBLLy) + { + header = new AsciiGridHeader(); + header.Cellsize = cellSize; + header.XCenter = !isBLLx; + if (isBLLx) + { + // Bottom left + double cellsizeHalf = cellSize / 2.0; + header.MinX = blX + cellsizeHalf; + header.MaxX = (blX + cellSize * (noX - PIXEL_OFFSET)) + cellsizeHalf; + } + else + { + // center + header.MinX = blX; + header.MaxX = blX + cellSize * (noX - PIXEL_OFFSET); + } + header.YCenter = !isBLLy; + if (isBLLy) + { + Double cellsizeHalf = cellSize / 2.0; + header.MinY = blY + cellsizeHalf; + header.MaxY = (blY + cellSize * (noY - PIXEL_OFFSET)) + cellsizeHalf; + } + else + { + header.MinY = blY; + header.MaxY = blY + cellSize * (noY - PIXEL_OFFSET); + } + header.NumX = noX; + header.NumY = noY; + header.NullVal = noDataVal; + + return header; + } + + /// + /// Creates the DataGrid + /// This doesn't handle xCenter / yCenter values just yet + /// + /// + /// + /// + /// Whether we have an xCorner or xCenter in the ascii file + /// Whether we have an xCorner or yCenter in the ascii file + private void GenerateLatLonValues(double xllCorner, double yllCorner, double cellSize, bool isBllX, bool isBllY) + { + List xScale = new List(); + List yScale = new List(); + double scale = xllCorner; + if (isBllX) + { + for (int j = 0; j < header.NumX; j++) + { + xScale.Add(scale); + scale += cellSize; + } + } + else + { + //TODO: handle if XCenter + } + + scale = yllCorner; + if (isBllY) + { + for (int j = 0; j < header.NumY; j++) + { + yScale.Add(scale); + scale += cellSize; + } + } + else + { + //TODO: handle if XCenter + } + // ASCII Grid starts at the lower left corner, DataGrid assumes the data starts at the upper left + // So we flip the Y order here so that it gets rendered correctly + yScale.Reverse(); + + this.DataGrid = new DataGrid(yScale.ToArray(), xScale.ToArray(), new DataGrid.Bounds(header.MinX, header.MaxX, header.MinY, header.MaxY)); + } + + /// + /// Reads ASCII Grid header + /// + /// + /// + protected override bool ReadDimensions(StreamReader streamReader) + { + bool headerRead = false; + + int noX = 0, noY = 0; + double noDataVal = -9999; + double blX = 0, blY = 0, cellSize = 0; + + bool isBLLx = true; + bool isBLLy = true; + + string line; + while ((line = streamReader.ReadLine()) != null) + { + var tokens = line.Split(new char[] { Delimiter }, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length == 2) + { + switch (tokens[0]) + { + case "ncols": + { + noX = Convert.ToInt32(tokens[1]); break; + } + case "nrows": + { + noY = Convert.ToInt32(tokens[1]); + break; + } + case "xllcenter": + { + isBLLx = false; + goto case "xllcorner"; + } + case "xllcorner": + { + blX = Convert.ToDouble(tokens[1]); + break; + } + case "yllcenter": + { + isBLLy = false; + goto case "yllcorner"; + } + case "yllcorner": + { + blY = Convert.ToDouble(tokens[1]); + break; + } + case "cellsize": + { + cellSize = Convert.ToDouble(tokens[1]); + break; + } + case "NODATA_value": + { + noDataVal = Convert.ToDouble(tokens[1]); + headerRead = true; // This is the last line of the header + break; + } + } + } + if (headerRead) break; + } + + populateHeader(noX, noY, blX, blY, noDataVal, cellSize, isBLLx, isBLLy); + + GenerateLatLonValues(blX, blY, cellSize, isBLLx, isBLLy); + + return headerRead; + } + + /// + /// Read z values and stores them in the grid + /// + /// ASCII grid stream (it points to the right location where z values start in the ASCII grid file) + /// + protected override bool ReadDataGrid(StreamReader streamReader) + { + bool readSome = false; + string line; + int y = 0; + + while ((line = streamReader.ReadLine()) != null) + { + var tokens = line.Split(Delimiter); + int x = 0; + foreach (var token in tokens) + { + if (x > 0) + { + // Read a z value + var z = token.Count() == 0 ? header.NullVal : Convert.ToDouble(token); + + // Store only genuine values and ignore the ones where there is no data + if (z != header.NullVal) + { + DataGrid.SetValue(x - 1, y, z); + } + + readSome = true; + } + + ++x; + } + + ++y; + } + + return readSome; + } + } + +} \ No newline at end of file diff --git a/Envitia.MapLink.Grids/Ascii/AsciiGridHeader.cs b/Envitia.MapLink.Grids/Ascii/AsciiGridHeader.cs new file mode 100644 index 0000000..b87bf41 --- /dev/null +++ b/Envitia.MapLink.Grids/Ascii/AsciiGridHeader.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Envitia.MapLink.Grids +{ + /// + /// Represents the header of an ASCII Grid File + /// + public struct AsciiGridHeader + { + public enum ByteOrder + { + LSBFirst, + MSBFirst, + VMS_FFloat + }; + + public double MinX { get; set; } + + public double MinY { get; set; } + + public double MaxX { get; set; } + + public double MaxY { get; set; } + + public int NumX { get; set; } + + public int NumY { get; set; } + + public ByteOrder Byteorder { get; set; } + + public double NullVal { get; set; } + + public bool YCenter { get; set; } + public bool XCenter { get; set; } + public double Cellsize { get; set; } + } +} diff --git a/Envitia.MapLink.Grids/ColourScales.cs b/Envitia.MapLink.Grids/ColourScales.cs new file mode 100644 index 0000000..afd8d44 --- /dev/null +++ b/Envitia.MapLink.Grids/ColourScales.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Envitia.MapLink.Grids.Data +{ + public class ContourColour + { + public double MinZ { get; set; } + public double MaxZ { get; set; } + public double Z { get; set; } + public System.Windows.Media.Color Color { get => System.Windows.Media.Color.FromArgb(A, R, G, B); } + public byte A { get; set; } + public byte R { get; set; } + public byte G { get; set; } + public byte B { get; set; } + + public ContourColour(double z, byte r, byte g, byte b) + { + Z = z; + R = r; + G = g; + B = b; + A = 255; + } + + public ContourColour(double z, byte r, byte g, byte b, byte a) + { + Z = z; + R = r; + G = g; + B = b; + A = a; + } + } + + public class ColourScales + { + private static ColourScales globalInstance = new ColourScales(); + public static ColourScales GlobalInstance { get { return globalInstance; } } + + private SortedDictionary> propertyColours = new SortedDictionary>(); + + private enum Tokens + { + TokenZ, + TokenR, + TokenG, + TokenB, + + NumTokens + } + + public static ContourColour ClosestTo(IEnumerable collection, double z) + { + return collection.OrderBy(x => Math.Abs(z - x.Z)).First(); + } + + public void Load(string property, string contourPath) + { + using (var reader = new System.IO.StreamReader(contourPath)) + { + List contourColours = new List(); + + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + var values = line.Split(','); + + if (values.Length == (int)Tokens.NumTokens) + { + contourColours.Add(new ContourColour( + Convert.ToDouble(values[(int)Tokens.TokenZ]), + Convert.ToByte(values[(int)Tokens.TokenR]), + Convert.ToByte(values[(int)Tokens.TokenG]), + Convert.ToByte(values[(int)Tokens.TokenB]) + )); + } + } + propertyColours[property] = contourColours; + } + } + public System.Windows.Media.Color GetColor(string property, double zValue) + { + var colours = propertyColours[property]; + var closestColour = ClosestTo(colours, zValue); + return closestColour.Color; + } + } +} diff --git a/Envitia.MapLink.Grids/Cube.cs b/Envitia.MapLink.Grids/Cube.cs new file mode 100644 index 0000000..db17fb2 --- /dev/null +++ b/Envitia.MapLink.Grids/Cube.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Envitia.MapLink.Grids +{ + public class Cube + { + public class ZDimension + { + public int Z { get; set; } + public DataGrid XY { get; } = new DataGrid(); + } + + public List ZDimensions { get; } = new List(); + + public Tuple GetValue(int x, int y, int z) + { + if (ZDimensions.Count == 0) + { + throw new Exception("Cube has no dimensions"); + } + + var found = ZDimensions.Find(delegate (ZDimension dim) + { + return dim.Z == z; + }); + + if (found == null) + { + // Outside z range. Not an exception. + return new Tuple(false, Double.NaN); + } + + return found.XY.GetClosestValue(x, y); + } + } +} diff --git a/Envitia.MapLink.Grids/DataGrid.cs b/Envitia.MapLink.Grids/DataGrid.cs new file mode 100644 index 0000000..b2c8187 --- /dev/null +++ b/Envitia.MapLink.Grids/DataGrid.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Envitia.MapLink.Grids +{ + /// + /// Models an x,y,z grid of data. + /// + public class DataGrid + { + public double[] Rows { get; private set; } + public double[] Columns { get; private set; } + + public int NumRows { get => Rows.Length; } + public int NumColumns { get => Columns.Length; } + + public double MinZ { get; private set; } = Double.MaxValue; + public double MaxZ { get; private set; } = Double.MinValue; + + public Bounds GridBounds { get; private set; } + + private Tuple[,] Grid { get; set; } + + public DataGrid() + { + } + + public DataGrid(double[] rows, double[] columns) + { + Rows = rows; + Columns = columns; + + Grid = new Tuple[NumRows, NumColumns]; + } + + public DataGrid(double[] rows, double[] columns, Bounds bounds) : this(rows, columns) + { + this.GridBounds = bounds; + } + + public Tuple GetValue(int x, int y) + { + if (x >= NumColumns) + { + throw new ArgumentOutOfRangeException("x"); + } + if (y >= NumRows) + { + throw new ArgumentOutOfRangeException("y"); + } + + return Grid[y,x]; + } + + public class Range + { + public double TripValue { get; set; } + public int StartIndex { get; set; } + public int EndIndex { get; set; } + + public List Trips { get; set; } + + public double GetTrip(Tuple value) + { + if (value == null || !value.Item1) + { + return Double.NaN; + } + + for (int i = 1; i < Trips.Count; ++i) + { + if (value.Item2 >= Trips[i-1] && value.Item2 < Trips[i]) + { + return Trips[i-1]; + } + + if (i == Trips.Count - 1 + && value.Item2 >= Trips[i]) + { + return Trips[i]; + } + } + return Double.NaN; + } + } + + public class Bounds + { + public double MinX { get; set; } = Double.NaN; + public double MaxX { get; set; } = Double.NaN; + public double MinY { get; set; } = Double.NaN; + public double MaxY { get; set; } = Double.NaN; + + List bounds; + + public Bounds(double minX, double maxX, double minY, double maxY) + { + MinX = minX; + MaxX = maxX; + MinY = minY; + MaxY = maxY; + + bounds = new List() { minX, minY, maxX, maxY}; + } + + public bool IsWithinBounds(double x, double y) + { + if (!bounds.Contains(Double.NaN)) + { + // Make sure we have genuine values for Min and Max (These are set only if they are present in the header) + return (x >= MinX && y >= MinY && x <= MaxX && y <= MaxY); + } + // If we haven't got the right min and max values, we just allow all values + return true; + } + } + + + + /// + /// Get a list of ranges along the axes that sit between trip values. + /// + /// The row to examine. + /// The list of triplines that separate the requested ranges. + /// List of X ranges. + public List GetXRanges(int y, List trips) + { + trips.Sort(); + + var ranges = new List(); + + var currentRange = new Range { StartIndex = 0, Trips = trips }; + var currentValue = GetValue(0, y); + currentRange.TripValue = currentRange.GetTrip(currentValue); + + for (int x = 1; x < NumColumns; ++x) + { + currentValue = GetValue(x, y); + var trip = currentRange.GetTrip(currentValue); + // Have we crossed one of the trips? + if (Double.IsNaN(trip) && Double.IsNaN(currentRange.TripValue)) + { + currentRange.StartIndex = x; + } + else if (trip != currentRange.TripValue) + { + if (currentRange.StartIndex < currentRange.EndIndex) + { + ranges.Add(currentRange); + } + + // Start a new range at the next index + currentRange = new Range { StartIndex = x, Trips = trips, TripValue = trip }; + } + else + { + if (Double.IsNaN(currentRange.TripValue)) + { + currentRange.StartIndex = x; + } + currentRange.EndIndex = x; + } + } + + if (!Double.IsNaN(currentRange.TripValue)) + { + // Add the final range + ranges.Add(currentRange); + } + + return ranges; + } + + public static int ClosestTo(double[] collection, double target) + { + // NB Method will return int.MaxValue for a sequence containing no elements. + // Apply any defensive coding here as necessary. + var closest = 0; + var minDifference = double.MaxValue; + bool minDifferenceSet = false; + for (int i = 0; i < collection.Length; ++i) + { + var difference = Math.Abs(collection[i] - target); + if (minDifference > difference) + { + minDifference = difference; + closest = i; + minDifferenceSet = true; + } + else if (minDifferenceSet && difference > minDifference) + { + // arrays are ordered low to high, so we know we can return now. + return closest; + } + } + + return closest; + } + + public Tuple GetClosestValue(double xVal, double yVal) + { + if ((bool)!GridBounds?.IsWithinBounds(xVal, yVal)) return null; + + if (xVal >= Columns.Last()) + { + //throw new ArgumentOutOfRangeException("x"); + } + if (yVal >= Rows.Last()) + { + //throw new ArgumentOutOfRangeException("y"); + } + + int closestXIndex = ClosestTo(Columns, xVal); + int closestYIndex = ClosestTo(Rows, yVal); + + return GetValue(closestXIndex, closestYIndex); + } + + public Tuple SetValue(int x, int y, double value) + { + if (x >= NumColumns) + { + throw new ArgumentOutOfRangeException("x"); + } + if (y >= NumRows) + { + throw new ArgumentOutOfRangeException("y"); + } + + MinZ = value < MinZ ? value : MinZ; + MaxZ = value > MaxZ ? value : MaxZ; + + return Grid[y, x] = new Tuple(true, value); + } + + /// + /// Get the profile at a given x position. Profile is the value of each row at a given x position. + /// + /// + /// The value at each row at the x position. + public List> GetProfile(int x) + { + var profile = new List>(NumRows); + + for (int rowNum = 0; rowNum < NumRows; ++rowNum) + { + var val = GetValue(x, rowNum); + if (val != null && val.Item1 && val.Item2 != 0.0) + { + profile.Add(new Tuple(Rows[rowNum], val.Item2)); + } + } + + return profile; + } + + /// + /// Populate the DataGrid from a MapLink terrain database. + /// + /// + /// + /// + /// + public bool FromTerrainDatabase(Envitia.MapLink.Terrain.TSLNTerrainDatabase terrainDatabase, int numColumns, int numRows) + { + double x1 = 0.0; + double x2 = 0.0; + double y1 = 0.0; + double y2 = 0.0; + + if (terrainDatabase.queryExtent(out x1, out y1, out x2, out y2) != Envitia.MapLink.Terrain.TSLNTerrainReturn.TSLNTerrain_OK) + { + return false; + } + + var dataItems = new Envitia.MapLink.Terrain.TSLNTerrainDataItem[numColumns * numRows]; + if (terrainDatabase.queryArea(x1, y1, x2, y2, numColumns, numRows, out dataItems) != Envitia.MapLink.Terrain.TSLNTerrainReturn.TSLNTerrain_OK) + { + return false; + } + + Rows = new double[numRows]; + Columns = new double[numColumns]; + Grid = new Tuple[NumRows, NumColumns]; + + for (int y = 0; y < numRows; ++y) + { + int terrainDatabaseY = numRows - (1 + y); + for (int x = 0; x < numColumns; ++x) + { + var dataItem = dataItems[(terrainDatabaseY * numColumns) + x]; + if (y == 0) + { + Columns[x] = dataItem.m_x; + } + if (x == 0) + { + Rows[y] = dataItem.m_y; + } + Grid[y, x] = new Tuple(!dataItem.m_isNull, dataItem.m_z); + } + } + + return true; + } + + public Envitia.MapLink.TSLNEnvelope GetEnvelope(Envitia.MapLink.TSLNDrawingSurface drawingSurface) + { + if (drawingSurface == null) + return null; + + if (Columns.Length < 3) + return null; + if (Rows.Length < 3) + return null; + + var tmcLeft = 0; + var tmcBottom = 0; + drawingSurface.latLongToTMC(Rows[0], Columns[0], out tmcLeft, out tmcBottom); + var tmcRight = 0; + var tmcTop = 0; + drawingSurface.latLongToTMC(Rows[Rows.Length-1], Columns[Columns.Length-1],out tmcRight, out tmcTop); + + var tmcLeftInternal = 0; + var tmcBottomInternal = 0; + drawingSurface.latLongToTMC(Rows[1], Columns[1], out tmcLeftInternal, out tmcBottomInternal); + + // Using the centre of the adjacent cell, find the border between the cells. + int xDistance = System.Math.Abs(tmcLeftInternal - tmcLeft); + int yDistance = System.Math.Abs(tmcBottomInternal - tmcBottom); + tmcLeft -= (int)((xDistance / 2.0) + 0.5); + tmcBottom -= (int)((yDistance / 2.0) + 0.5); + + var tmcRightInternal = 0; + var tmcTopInternal = 0; + drawingSurface.latLongToTMC(Rows[Rows.Length - 2], Columns[Columns.Length - 2], out tmcRightInternal, out tmcTopInternal); + + xDistance = System.Math.Abs(tmcRight - tmcRightInternal); + yDistance = System.Math.Abs(tmcTop - tmcTopInternal); + tmcRight += (int)((xDistance / 2.0) + 0.5); + tmcTop += (int)((yDistance / 2.0) + 0.5); + + return new Envitia.MapLink.TSLNEnvelope(tmcLeft, tmcBottom, tmcRight, tmcTop); + } + } +} diff --git a/Envitia.MapLink.Grids/DepthGrid.cs b/Envitia.MapLink.Grids/DepthGrid.cs new file mode 100644 index 0000000..38e9bdd --- /dev/null +++ b/Envitia.MapLink.Grids/DepthGrid.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Windows; + +namespace Envitia.MapLink.Grids +{ + /// + /// A DataGrid where each row is a depth. + /// + public class DepthGrid + { + public DataGrid Grid { get; private set; } + + private const int NUM_COLUMNS = 50; + + /// + /// Construct a depth grid from a set of terrain database. + /// + /// Depths (in metres) + the terrain database for each depth. + /// + /// + /// + /// + public DepthGrid(List> depthValues, double x1, double y1, double x2, double y2) + { + List depths = new List(); + foreach (var depth in depthValues) + { + depths.Add(depth.Item1); + } + + for (int y = 0; y < depthValues.Count; ++y) + { + Envitia.MapLink.Terrain.TSLNTerrainDataItem[] dataItems = new Envitia.MapLink.Terrain.TSLNTerrainDataItem[NUM_COLUMNS]; + + if (depthValues[y].Item2 != null + && depthValues[y].Item2.queryLine(x1,y1,x2,y2, NUM_COLUMNS, out dataItems) == Envitia.MapLink.Terrain.TSLNTerrainReturn.TSLNTerrain_OK) + { + if (Grid == null) + { + List columns = new List(); + for (int n = 0; n < dataItems.Length; ++n) + { + columns.Add(dataItems[n].m_x); + } + + Grid = new DataGrid(depths.ToArray(), columns.ToArray()); + } + + for (int x = 0; x < NUM_COLUMNS; ++x) + { + Grid.SetValue(x, y, dataItems[x].m_z); + } + } + } + } + + /// + /// Construct a depth grid from a set of DataGrids + /// + /// Depths (in metres) + the xy Grid for each depth. + /// + /// + /// + /// + public DepthGrid(List> depthValues, double x1, double y1, double x2, double y2) + { + List depths = new List(); + foreach (var depth in depthValues) + { + depths.Add(depth.Item1); + } + + Point startCoord = new Point(x1, y1); + Point endCoord = new Point(x2, y2); + Line line = new Line(startCoord, endCoord); + Point[] points = line.getPoints(NUM_COLUMNS); + List columns = new List(); + + Point firstPoint = startCoord; + foreach(var point in points) + { + // columns of the DataGrid are the distance (m) along the line [ Start Coord, End Coord ] + columns.Add(TSLNCoordinateConverter.greatCircleDistance(firstPoint.X, firstPoint.Y, point.X, point.Y)); + } + + for (int y = 0; y < depthValues.Count; ++y) + { + List dataItems = new List(NUM_COLUMNS); + + if (depthValues[y].Item2 != null) + { + if(Grid == null) + { + Grid = new DataGrid(depths.ToArray(), columns.ToArray(), depthValues[y].Item2.GridBounds); + } + foreach (var point in points) + { + var closestValue = depthValues[y].Item2.GetClosestValue(point.X, point.Y); + // Is there a value at this pixel? If not, leave the pixel unset. + if (closestValue != null + && closestValue.Item1 + && closestValue.Item2 >= 0) + { + dataItems.Add(closestValue.Item2); + } + else + { + // The point could be out of range for the Grid and we don't want to show this on the graph + dataItems.Add(double.NaN); + } + } + + for (int x = 0; x < NUM_COLUMNS; ++x) + { + Grid.SetValue(x, y, dataItems[x]); + } + } + } + } + } +} diff --git a/Envitia.MapLink.Grids/Envitia.MapLink.Grids.csproj b/Envitia.MapLink.Grids/Envitia.MapLink.Grids.csproj new file mode 100644 index 0000000..4b63249 --- /dev/null +++ b/Envitia.MapLink.Grids/Envitia.MapLink.Grids.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {17453A68-5CCA-4035-A57E-E53BE4AF0469} + Library + Properties + Envitia.MapLink.Grids + Envitia.MapLink.Grids + v4.8 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + $(MAPL_PATH)\Envitia.MapLink.NativeHelpers.dll + + + $(MAPL_PATH)\Envitia.MapLink.Terrain64.dll + + + $(MAPL_PATH)\Envitia.MapLink64.dll + + + $(MAPL_PATH)\Envitia.MapLinkEx64.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Envitia.MapLink.Grids/Filter.cs b/Envitia.MapLink.Grids/Filter.cs new file mode 100644 index 0000000..c25ee4c --- /dev/null +++ b/Envitia.MapLink.Grids/Filter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Envitia.MapLink.Grids +{ + /// + /// Simple filter class that identifies values plus or minus some Range relative to a ReferenceValue. + /// + public class Filter + { + public double ReferenceValue { get; set; } + public double Range { get; set; } + public bool Include(double z) + { + return z >= (ReferenceValue + Range) + || z <= (ReferenceValue - Range); + } + } +} diff --git a/Envitia.MapLink.Grids/GridDataset.cs b/Envitia.MapLink.Grids/GridDataset.cs new file mode 100644 index 0000000..c280628 --- /dev/null +++ b/Envitia.MapLink.Grids/GridDataset.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Envitia.MapLink.Grids +{ + /// + /// Loads an ASCII grid file + /// + public abstract class GridDataset + { + + public char Delimiter { get; set; } = ' '; + + public DataGrid DataGrid { get; protected set; } + + public double NoData { get; set; } = Double.NaN; + + /// + /// Reads Grid file dimensiona + /// + /// + /// + protected abstract bool ReadDimensions(StreamReader streamReader); + + /// + /// Read z values and stores them in the grid + /// + /// grid file stream (it points to the right location where z values start in the grid file) + /// + protected abstract bool ReadDataGrid(StreamReader streamReader); + + /// + /// Loads a grid file + /// + /// Path to the grid file + /// + public virtual bool Load(string gridFile) + { + const Int32 BufferSize = 4096; + var fileStream = System.IO.File.OpenRead(gridFile); + + bool readSome = false; + using (var streamReader = new System.IO.StreamReader(fileStream, Encoding.UTF8, true, BufferSize)) + { + readSome = ReadDimensions(streamReader); + readSome &= ReadDataGrid(streamReader); + } + + return readSome; + } + } +} diff --git a/Envitia.MapLink.Grids/GridLayer.cs b/Envitia.MapLink.Grids/GridLayer.cs new file mode 100644 index 0000000..48bb83a --- /dev/null +++ b/Envitia.MapLink.Grids/GridLayer.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Envitia.MapLink.Grids +{ + /// + /// A GridLayer visualises a DataGrid as a geo-referenced rectangular grid. + /// Renders the grid into an off-screen bitmap which is then rendered to the drawing surface using the data layers's rendering interface. + /// + public class GridLayer : Envitia.MapLink.TSLNClientCustomDataLayer + { + /// + /// The data grid to visualise. + /// + public DataGrid Grid { get; set; } + + /// + /// The underlying custom data layer. + /// + public Envitia.MapLink.TSLNCustomDataLayer DataLayer { get; } = new TSLNCustomDataLayer(); + + /// + /// A maximum height in pixels for the rendered bitmap. + /// The bitmap will be created with a height equalling this value or the grid's row count, + /// whichever is smaller. + /// Defaults to 800. + /// + public int MaxBitmapHeight { get; set; } = 800; + + /// + /// A maximum width in pixels for the rendered bitmap. + /// The bitmap will be created with a width equalling this value or the grid's column count, + /// whichever is smaller. + /// Defaults to 1200. + /// + public int MaxBitmapWidth { get; set; } = 1200; + + private System.Drawing.Bitmap Bitmap { get; set; } + + public GridLayer() + { + // Link the custom data layer to the client layer (this). + DataLayer.setClientCustomDataLayer(this); + } + + /// + /// Maps a cell value to a pixel colour. + /// Override this to provide other colour mappings. + /// + /// The cell value + /// The mapped colour. + public virtual System.Drawing.Color ValueToColour(double val) + { + // TODO: Use some colour configuration + var color = Data.ColourScales.GlobalInstance.GetColor("Temperature", val); + + if (color == null) + { + return System.Drawing.Color.FromArgb(255, System.Drawing.Color.White); + } + else + { + return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); + } + } + + /// + /// Get the pixel value for the given lat/long coordinate. + /// + /// + /// + /// Tuple: item1: true if there is a value for this coordinate, false if not (i.e. empty pixel); double: the pixel value. + public virtual Tuple GetPixelValue(double longitude, double latitude) + { + return Grid?.GetClosestValue(longitude, latitude); + } + + /// + /// Get the TMC envelope of the grid on the drawing surface. + /// The method is used to determine whether the grid's extent intersects with the viewable extent, and therefore whether to bother with any rendering. + /// + /// + /// The grid's TMC rendering extent on the given drawing surface. + public virtual Envitia.MapLink.TSLNEnvelope GetEnvelope(Envitia.MapLink.TSLNDrawingSurface drawingSurface) + { + return Grid?.GetEnvelope(drawingSurface); + } + + /// + /// Create the off-screen bitmap. + /// + /// The drawing surface the bitmap will be rendered into. + /// (xy) device unit coordinates for the bottom left of the bitmap. + /// (xy) device unit coordinates for the top right of the bitmap. + /// The off-screen bitmap. + /// The method throws an exception if the drawing surface is null. + private System.Drawing.Bitmap CreateBitmap(Envitia.MapLink.TSLNDrawingSurface drawingSurface, Tuple duBottomLeft, Tuple duTopRight) + { + if (drawingSurface == null) + throw new ArgumentNullException("drawingSurface"); + + // Get the bitmap's dimensions + int width = Math.Abs(duTopRight.Item1 - duBottomLeft.Item1) + 1; + int height = Math.Abs(duTopRight.Item2 - duBottomLeft.Item2) + 1; + int bitmapWidth = Math.Min(width, MaxBitmapWidth); + int bitmapHeight = Math.Min(height, MaxBitmapHeight); + + // Pixel size in device units + double pixelStepX = width / (double)bitmapWidth; + double pixelStepY = height / (double)bitmapHeight; + + // Create a new off-screen, 32 bits-per-pixel, RGB bitmap with alpha channel for transparency. + var bitmap = new System.Drawing.Bitmap(bitmapWidth, bitmapHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + // Iterate over the pixels along x axis + for (int x = 0; x < bitmapWidth; ++x) + { + // The x-axis pixel in device units + int dux = (int)(duBottomLeft.Item1 + (x * pixelStepX)); + + // Iterate over the pixels along the y axis + for (int y = 0; y < bitmapHeight; ++y) + { + // The y-axis pixel in device units + int duy = (int)(duBottomLeft.Item2 + (y * pixelStepY)); + + // Device units to lat/long coordinate. TODO: faster to use TMC coordinates rather than lat/long? + drawingSurface.DUToLatLong(dux, duy, out double latitude, out double longitude); + + // Get the grid value for this pixel. + var closestVal = GetPixelValue(longitude, latitude); + + // Is there a value at this pixel? If not, leave the pixel unset. + if (closestVal != null + && closestVal.Item1 + && closestVal.Item2 >= 0) + { + // Set the pixel's value. + bitmap.SetPixel(x, bitmapHeight - y - 1, ValueToColour(closestVal.Item2)); + } + } + } + + return bitmap; + } + + /// + /// Force a re-creation of the bitmap so that any changes are picked up. + /// + public void Reset() + { + Bitmap = null; + } + + private TSLNEnvelope PreviousDrawnEnvelope { get; set; } + + private bool IsCreateNeeded(TSLNEnvelope renderingExtent) + { + if (Bitmap == null) + return true; + + if (PreviousDrawnEnvelope == null) + return true; + + // Maintain a reasonable rendering quality at different zoom levels without recreating the bitmap on every render. + if (renderingExtent.width > PreviousDrawnEnvelope.width * 2) + return true; + if (renderingExtent.width < PreviousDrawnEnvelope.width * .5) + return true; + + return false; + } + + /// + /// Implementation of TSLNClientCustomDataLayer.drawLayer(). + /// Renders the grid to an off-screen bitmap which is then rendered to the surface using the data layer's rendering interface. + /// + /// The layer's rendering interface. + /// Display area extent. + /// Data layer stuff, including the drawing surface we are rendering into. + /// + public override bool drawLayer(TSLNRenderingInterface renderingInterface, TSLNEnvelope extent, TSLNCustomDataLayerHandler layerHandler) + { + if (layerHandler.drawingSurface == null) + { + return false; + } + + // Does the grid intersect with the display area? + var gridEnvelope = GetEnvelope(layerHandler.drawingSurface); + if (!renderingInterface.extent.intersect(gridEnvelope)) + { + // Not an error, just an optimisation. + return true; + } + + // Convert the grid extent from TMC to device units. + layerHandler.drawingSurface.TMCToDU(gridEnvelope.bottomLeft.x, gridEnvelope.bottomLeft.y, out int bottomLeftDux, out int bottomLeftDuy); + layerHandler.drawingSurface.TMCToDU(gridEnvelope.topRight.x, gridEnvelope.topRight.y, out int topRightDux, out int topRightDuy); + + // Make sure the devie unit extent is the right way round. + if (bottomLeftDux > topRightDux) + { + (bottomLeftDux, topRightDux) = (topRightDux, bottomLeftDux); + } + if (bottomLeftDuy > topRightDuy) + { + (bottomLeftDuy, topRightDuy) = (topRightDuy, bottomLeftDuy); + } + + // Only create the bitmap when absolutely necessary. + if (IsCreateNeeded(renderingInterface.extent)) + { + // Create the off-screen bitmap. + Bitmap = CreateBitmap(layerHandler.drawingSurface, new Tuple(bottomLeftDux, bottomLeftDuy), new Tuple(topRightDux, topRightDuy)); + if (Bitmap == null) + return false; + + // Remember this extent - used by IsCreateNeeded(). + PreviousDrawnEnvelope = renderingInterface.extent; + } + + // Draw the bitmap using the layer's rendering interface. + renderingInterface.graphics().DrawImage(Bitmap, bottomLeftDux, topRightDuy, topRightDux - bottomLeftDux, bottomLeftDuy - topRightDuy); + + // All good. + return true; + } + } +} diff --git a/Envitia.MapLink.Grids/Line.cs b/Envitia.MapLink.Grids/Line.cs new file mode 100644 index 0000000..248eb5a --- /dev/null +++ b/Envitia.MapLink.Grids/Line.cs @@ -0,0 +1,43 @@ +using System.Windows; + +namespace Envitia.MapLink.Grids +{ + /// + /// Represent a straight line between two given Co-ordinates + /// + internal class Line + { + public Point p1, p2; + + public Line(Point p1, Point p2) + { + this.p1 = p1; + this.p2 = p2; + } + + /// + /// Returns a set of equally spaced Points within the line + /// + /// Number of Points to return + /// + public Point[] getPoints(int quantity) + { + var points = new Point[quantity]; + double ydiff = p2.Y - p1.Y, xdiff = p2.X - p1.X; + double slope = (double)(p2.Y - p1.Y) / (p2.X - p1.X); + double x, y; + + --quantity; + + for (double i = 0; i < quantity; i++) + { + y = slope == 0 ? 0 : ydiff * (i / quantity); + x = slope == 0 ? xdiff * (i / quantity) : y / slope; + points[(int)i] = new Point(x + p1.X, y + p1.Y); + } + + points[quantity] = p2; + return points; + } + } +} diff --git a/Envitia.MapLink.Grids/Properties/AssemblyInfo.cs b/Envitia.MapLink.Grids/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..254c742 --- /dev/null +++ b/Envitia.MapLink.Grids/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Envitia.MapLink.Grids")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Envitia.MapLink.Grids")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("17453a68-5cca-4035-a57e-e53be4af0469")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Envitia.MapLink.Grids/RadialGridLayer.cs b/Envitia.MapLink.Grids/RadialGridLayer.cs new file mode 100644 index 0000000..b24fca7 --- /dev/null +++ b/Envitia.MapLink.Grids/RadialGridLayer.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Envitia.MapLink.Grids +{ + /// + /// Specialisation of GridLayer that draws values from a series of radial grids propagating out from a central lat/long coordinate. + /// + public class RadialGridLayer : GridLayer + { + /// + /// The latitudinal (y) coordinate of the central point. + /// + public double CentreLatitude { get; set; } + /// + /// The longitudinal (x) coordinate of the central point. + /// + public double CentreLongitude { get; set; } + + /// + /// Tuple returned by GetPixelValue() indicating a pixel with no grid value. + /// + public static Tuple NoResult { get; } = new Tuple(false, Double.NaN); + + /// + /// The radial grids. This class does not use the GridLayer.Grid property. + /// The double field is the radial's bearing, in degrees, from the central point. + /// Each DataGrid should have XZ axes, with the X axis being metres from the central point, and the Z axis being some other property (e.g. depth or height). + /// + private SortedDictionary Radials { get; set; } + + private double[] Bearings { get; set; } + + /// + /// The maximum distance from the central point that the radial grids extend to. + /// + public double MaxDistance() + { + var maxDistance = Double.MinValue; + foreach (var bearing in Radials) + { + maxDistance = Math.Max(maxDistance, bearing.Value.Columns.Max()); + } + return maxDistance; + } + + public RadialGridLayer() + { + } + + /// + /// Set the radial grids. This class does not use the GridLayer.Grid property. + /// + /// The double field is the radial's bearing, in degrees, from the central point. + /// Each DataGrid should have XZ axes, with the X axis being metres from the central point, and the Z axis being some other property (e.g. depth or height). + public void SetRadials(SortedDictionary radialGrids) + { + Radials = new SortedDictionary(); + + foreach (var radial in radialGrids) + { + Radials.Add(radial.Key, radial.Value); + + // Duplicate the grid at a full circumference, to ensure the closest radial to a bearing is always found + // (e.g. so that a bearing of 359 matches a bearing specifed as 0, not 290). + Radials.Add(radial.Key + 360.0, radial.Value); + } + + // Cache all the bearings. + Bearings = Radials.Keys.ToArray(); + } + + /// + /// Get the radial grid whose bearing is closest to the given bearing. + /// + /// The bearing in degrees to find. + /// The radial grid whose bearing is closest to the given bearing. + private DataGrid ClosestRadialGrid(double bearing) + { + int closestIndex = DataGrid.ClosestTo(Bearings, bearing); + if (closestIndex == Double.MaxValue) + { + return null; + } + + return Radials[Bearings[closestIndex]]; + } + + /// + /// Override of GridLayer.GetEnvelope. + /// Calculated the extent based upon the maximum distances that the radials extend out from the centre. + /// + /// + /// Bounding rectangular envelope of all the radials. + public override TSLNEnvelope GetEnvelope(TSLNDrawingSurface drawingSurface) + { + var maxDistance = MaxDistance(); + + // Calculate the furthest points from the centre at each compass point. + double dummy = 0; + TSLNCoordinateConverter.greatCircleDistancePoint(CentreLatitude, CentreLongitude, 0, maxDistance, out double north, out dummy); + TSLNCoordinateConverter.greatCircleDistancePoint(CentreLatitude, CentreLongitude, 90, maxDistance, out dummy, out double east); + TSLNCoordinateConverter.greatCircleDistancePoint(CentreLatitude, CentreLongitude, 180, maxDistance, out double south, out dummy); + TSLNCoordinateConverter.greatCircleDistancePoint(CentreLatitude, CentreLongitude, 270, maxDistance, out dummy, out double west); + + // Convert the lat/long coordinates into TMCs + drawingSurface.latLongToTMC(south, west, out int tmcLeft, out int tmcBottom); + drawingSurface.latLongToTMC(north, east, out int tmcRight, out int tmcTop); + + // Create the bounding rectangular envelope. + return new TSLNEnvelope(new TSLNCoord(tmcLeft, tmcBottom), new TSLNCoord(tmcRight, tmcTop)); + } + + /// + /// Override of GridLayer.GetPixelValue(). + /// Get the pixel value for the given lat/long coordinate. The pixel value is found using the distance and bearing of the coordinate from the central point, + /// getting the closest radial grid to the bearing, and finally getting the closest XZ-axis value from the radial grid. + /// + /// + /// + /// Tuple: item1: true if there is a value for this coordinate, false if not (i.e. empty pixel); double: the pixel value. + /// Returns NoResult if the coordinates do not locate a pixel value. + public override Tuple GetPixelValue(double longitude, double latitude) + { + // Get the distance and bearing of this coordinate from the central point. + Envitia.MapLink.TSLNCoordinateConverter.greatCircleDistance(CentreLatitude, CentreLongitude, latitude, longitude, out double distance, out double bearing); + + // Check distance is within the maximum range. + if (distance > MaxDistance()) + { + return NoResult; + } + + // Get the closest radial grid to the bearing. + DataGrid radial = ClosestRadialGrid(bearing); + if (radial == null) + { + return NoResult; + } + + // Ask the grid for a pixel value. + // TODO: The y value should be selectable (e.g. depth or height). + return radial.GetClosestValue(distance, 10); + } + } +} diff --git a/MapLinkProApp/App.config b/MapLinkProApp/App.config index ea188d6..c10a5ea 100644 --- a/MapLinkProApp/App.config +++ b/MapLinkProApp/App.config @@ -5,7 +5,9 @@ + + \ No newline at end of file diff --git a/MapLinkProApp/App.xaml b/MapLinkProApp/App.xaml index b68a4e5..77c8e26 100644 --- a/MapLinkProApp/App.xaml +++ b/MapLinkProApp/App.xaml @@ -264,7 +264,135 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MapLinkProApp/ColourScales.xml b/MapLinkProApp/ColourScales.xml new file mode 100644 index 0000000..743d9c3 --- /dev/null +++ b/MapLinkProApp/ColourScales.xml @@ -0,0 +1,11 @@ + + + + Sound Speed + .\soundspeed.ctr + + + Temperature + .\temperature.ctr + + diff --git a/MapLinkProApp/CrossSectionPanel.cs b/MapLinkProApp/CrossSectionPanel.cs new file mode 100644 index 0000000..93e811b --- /dev/null +++ b/MapLinkProApp/CrossSectionPanel.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using DrawingSurfacePanel; +using Envitia.MapLink; +using Envitia.MapLink.Grids; + +namespace MapLinkProApp +{ + public record struct CrossSection(Tuple SliceStart, Tuple SliceEnd, Maps Maps, MapViewerPanel MapPanel, string Property); + + /// + /// DrawingVisualElement provides an element onto which the map can be drawn. + /// + public class DrawingVisualElement : FrameworkElement + { + private System.Windows.Media.VisualCollection _children; + + public System.Windows.Media.DrawingVisual drawingVisual; + + public DrawingVisualElement() + { + _children = new System.Windows.Media.VisualCollection(this); + + drawingVisual = new System.Windows.Media.DrawingVisual(); + _children.Add(drawingVisual); + } + + protected override int VisualChildrenCount + { + get { return _children.Count; } + } + + protected override System.Windows.Media.Visual GetVisualChild(int index) + { + if (index < 0 || index >= _children.Count) + throw new ArgumentOutOfRangeException(); + + return _children[index]; + } + } + + /// + /// CrossSectionPanel is the vertical slice (xz) view. + /// The class implements DrawingSurfacePanel.DrawLineInterationMode.IObserver so that it can be notified when a new vertical slice is selected in the xy view. + /// The panel draws a grid of data using .Net drawing classes. It does not use MapLink for drawing. + /// + public class CrossSectionPanel : System.Windows.Controls.Panel, DrawingSurfacePanel.DrawLineInterationMode.IObserver + { + public class Distance + { + const double MetresPerNauticalMile = 1852; + + public Tuple StartLatLon { get; set; } + public Tuple EndLatLon { get; set; } + + public double DistanceInMetres() + { + return TSLNCoordinateConverter.greatCircleDistance(StartLatLon.Item2, StartLatLon.Item1, EndLatLon.Item2, EndLatLon.Item1); + } + + public double DistanceInNautical() + { + return DistanceInMetres() / MetresPerNauticalMile; + } + } + + private DrawingVisualElement drawingVisualElement = new DrawingVisualElement(); + + public DataGrid Grid { get; set; } = null; + + public double IndicatedDepth { get; set; } = Double.NaN; + + public Size GridSize { get; set; } + + public bool UniformRowHeights { get; set; } = false; + + public CrossSection CrossSection { get; private set; } + + protected Rect GridRect { get; set; } + + public CrossSectionPanel() + { + base.Children.Add(drawingVisualElement); + } + + public void Initialise(CrossSection crossSection) + { + CrossSection = crossSection; + DepthGrid depthGrid = new DepthGrid(CrossSection.Maps.DepthGridValues(CrossSection.Property), CrossSection.SliceStart.Item1, CrossSection.SliceStart.Item2, CrossSection.SliceEnd.Item1, CrossSection.SliceEnd.Item2); + Grid = depthGrid.Grid; + + CrossSection.MapPanel.DrawLineInterationMode.Observers.Add(this); + } + + protected double GetCellHeight(int y, double gridHeight) + { + if (UniformRowHeights) + { + return y * (gridHeight / (double)Grid.NumRows); + } + + double depthExtent = Grid.Rows.Last(); + double oneUnitHeight = gridHeight / depthExtent; + double rowDifference = y > 0 ? Grid.Rows[y] - Grid.Rows[y - 1] : Grid.Rows[y]; + return (oneUnitHeight * rowDifference); + } + + protected virtual void DrawSlice(DrawingContext drawingContext) + { + // Noop by default + } + + protected virtual Tuple GetXRange() + { + return new Tuple(0, 0); + } + + protected virtual Tuple GetYRange() + { + return new Tuple(CrossSection.Maps.AllDepths.Min(), CrossSection.Maps.AllDepths.Max()); + } + + public void Draw() + { + if (Grid == null) + { + return; + } + if (CrossSection.Property.Length == 0) + { + return; + } + if (GridSize.Width == 0 || GridSize.Height == 0) + { + return; + } + + const int bigScaleMarginPixels = 50; + const int smallScaleMarginPixels = 15; + + GridRect = new Rect( + bigScaleMarginPixels, + bigScaleMarginPixels, + GridSize.Width - bigScaleMarginPixels - smallScaleMarginPixels, + GridSize.Height - bigScaleMarginPixels - smallScaleMarginPixels); + + var drawingContext = drawingVisualElement.drawingVisual.RenderOpen(); + + DrawSlice(drawingContext); + + System.Windows.Media.Pen blackPen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Black, 2); + blackPen.DashStyle = System.Windows.Media.DashStyles.Solid; + + drawingContext.DrawLine(blackPen, GridRect.TopLeft, GridRect.TopRight); // Line at the top + + drawingContext.DrawLine(blackPen, GridRect.BottomLeft, GridRect.BottomRight); // X-Coordinate + + const double numXCoords = 6.0; + double xInterval = (GridRect.Right - GridRect.Left) / numXCoords; // Interval between x axis coordinates + + Tuple xRange = GetXRange(); + + double xIncrement = (xRange.Item2 - xRange.Item1) / numXCoords; // Actual distance between each x value + + // Draw x-axis labels + double xCoord = GridRect.Left; + double xVal = xRange.Item1; + for (int i = 0; i <= numXCoords; ++i) + { + Point coord = new Point(xCoord, GridRect.Bottom + 10); + FormattedText text = new FormattedText(Math.Round(xVal, 1).ToString(), System.Globalization.CultureInfo.GetCultureInfo("en-US"), FlowDirection.LeftToRight, + new Typeface("LillyUPC"), 10, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip); + drawingContext.DrawText(text, coord); + Point lineCoord1 = new Point(xCoord, GridRect.Bottom); + Point lineCoord2 = new Point(xCoord, GridRect.Bottom + 10); + drawingContext.DrawLine(blackPen, lineCoord1, lineCoord2); + xCoord += xInterval; + xVal += xIncrement; + } + + drawingContext.DrawLine(blackPen, GridRect.BottomLeft, GridRect.TopLeft); // Y-Coordinate + + Tuple yRange = GetYRange(); + + // Draw Y-axis labels + const int numYCoords = 6; + double yInterval = (GridRect.Top - GridRect.Bottom) / numYCoords; + double yCoord = GridRect.Bottom; + double depth = yRange.Item2; + double depthInterval = (yRange.Item2 - yRange.Item1) / numYCoords; + for (int i = 0; i <= numYCoords; ++i) + { + Point coord = new Point(GridRect.Left - 40, yCoord); + FormattedText text = new FormattedText(Math.Round(depth, 1).ToString(), System.Globalization.CultureInfo.GetCultureInfo("en-US"), FlowDirection.LeftToRight, + new Typeface("LillyUPC"), 10, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip); + drawingContext.DrawText(text, coord); + Point lineCoord1 = new Point(GridRect.Left, yCoord); + Point lineCoord2 = new Point(GridRect.Left - 10, yCoord); + drawingContext.DrawLine(blackPen, lineCoord1, lineCoord2); + yCoord += yInterval; + depth -= depthInterval; + } + + drawingContext.Close(); + } + + /// + /// Updates Depth Grid to match new layer and redraws the vertical slice + /// + /// + public void UpdateCrossSection(string layer) + { + CrossSection = CrossSection with { Property = layer }; + UpdateCrossSection(); + } + + private void UpdateCrossSection() + { + DepthGrid depthGrid = new DepthGrid( + CrossSection.Maps.DepthGridValues(CrossSection.Property), + CrossSection.SliceStart.Item1, + CrossSection.SliceStart.Item2, + CrossSection.SliceEnd.Item1, + CrossSection.SliceEnd.Item2); + + Grid = depthGrid.Grid; + + Draw(); + } + + private double roundValue(double value) + { + double rem = value % 10; + return rem >= 5 ? (value - rem + 10) : (value - rem); + } + + // Override the default Measure method of Panel + protected override Size MeasureOverride(Size availableSize) + { + Size panelDesiredSize = new Size(300, 200); + return panelDesiredSize; + } + protected override Size ArrangeOverride(Size finalSize) + { + GridSize = finalSize; + + Draw(); + + return finalSize; // Returns the final Arranged size + } + + /// + /// Handles notification when the Vertical Slice changes + /// + /// Start co-ordinate of the new slice in MU + /// End co-ordinate of the new slice in MU + void DrawLineInterationMode.IObserver.NewLine(Tuple startMu, Tuple endMu) + { + CrossSection.Maps.FoundationLayer.GetDataLayer().coordinateSystem.MUToLatLong(startMu.Item1, startMu.Item2, out double startLat, out double startLon); + CrossSection.Maps.FoundationLayer.GetDataLayer().coordinateSystem.MUToLatLong(endMu.Item1, endMu.Item2, out double endLat, out double endLon); + + CrossSection = CrossSection with + { + SliceStart = new Tuple(startLon, startLat), + SliceEnd = new Tuple(endLon, endLat) + }; + UpdateCrossSection(); + } + + void DrawLineInterationMode.IObserver.Invalidate() + { + } + } +} diff --git a/MapLinkProApp/DepthProfile.xaml b/MapLinkProApp/DepthProfile.xaml new file mode 100644 index 0000000..f48dc30 --- /dev/null +++ b/MapLinkProApp/DepthProfile.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + Depth Profile + + + + + + + + + + + + + + + + + + + + + + diff --git a/MapLinkProApp/DepthProfile.xaml.cs b/MapLinkProApp/DepthProfile.xaml.cs new file mode 100644 index 0000000..c628c5d --- /dev/null +++ b/MapLinkProApp/DepthProfile.xaml.cs @@ -0,0 +1,89 @@ +using Envitia.MapLink.Grids; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Controls; + +namespace MapLinkProApp +{ + /// + /// Interaction logic for Window1.xaml + /// + public partial class DepthProfileWindow : Window + { + public DockableWindow Dockable { get; private set; } + + private LayerSelector LayerSelector { get; set; } + + public DepthProfileWindow(DockableWindow dockable) + { + InitializeComponent(); + + Dockable = dockable; + Dockable.Window = this; + + LayerSelector = new LayerSelector(this); + LayerSelector.LayerChanged += OnLayerChanged; + } + + /// + /// Initialises vertical slice view + /// + public void Initialise(CrossSection crossSection) + { + StackPanel layersPanel = (StackPanel)this.FindName("DepthProfileLayersPanel"); + List layers = new List(); + // Layer selector shows all distinct layer properties with a valid depth + crossSection.Maps.MapLayers.FindAll(x => x.Depth != Double.MaxValue).Select(x => x.Property).Distinct().ToList().ForEach(layer => + { + layers.Add(new LayerSelector.LayerDetails { Name = layer, Label = layer, Index = -1 }); + }); + LayerSelector.AddLayerToSelectionPanel(layersPanel, layers); + + ProfilePanel.Initialise(crossSection); + } + + private void Minimise_Click(object sender, RoutedEventArgs e) + { + Dockable.RaiseMinimiseEvent(); + } + + private void Dock_Click(object sender, RoutedEventArgs e) + { + Dockable.RaiseDockEvent(); + } + + private void Maximise_Click(object sender, RoutedEventArgs e) + { + Dockable.RaiseMaximiseEvent(); + } + + /// + /// Show or hides the layer selection panel + /// + /// + /// + private void LayersButton_Click(object sender, RoutedEventArgs e) + { + StackPanel element = (StackPanel)this.FindName("DepthProfileLayersPanel"); + + Visibility visibility = element.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; + element.Visibility = visibility; + } + + /// + /// Handles notification when an overlay changes + /// + /// + /// + public void OnLayerChanged(object sender, EventArgs e) + { + LayerSelector layerSelector = (LayerSelector)sender; + string layer = layerSelector.SelectedLayer; + + ProfilePanel.UpdateCrossSection(layer); + } + } +} diff --git a/MapLinkProApp/DepthProfilePanel.cs b/MapLinkProApp/DepthProfilePanel.cs new file mode 100644 index 0000000..b913e10 --- /dev/null +++ b/MapLinkProApp/DepthProfilePanel.cs @@ -0,0 +1,83 @@ +using Envitia.MapLink; +using Envitia.MapLink.Grids; +using System; +using System.Linq; +using System.Windows; +using System.Windows.Media; + +namespace MapLinkProApp +{ + /// + /// This class handles Depth Profile view + /// The panel draws the depth profile of a grid of data using .Net drawing classes + /// + public class DepthProfilePanel : CrossSectionPanel + { + Data.Profile Profile { get; set; } = new Data.Profile(); + + /// + /// Calculates the pixel y position offset for a real y value. + /// + /// + /// + /// + private double ToYPosition(double depth, double gridHeight) + { + double yExtent = Grid.Rows.Last(); + double oneYUnit = gridHeight / yExtent; + return (oneYUnit * depth); + } + + /// + /// Calculates the pixel x position offset for a real x value. + /// + /// + /// + /// + private double ToXPosition(double x, double gridWidth) + { + double adjustedX = x - CrossSection.SliceStart.Item1; + double xExtent = CrossSection.SliceEnd.Item1 - CrossSection.SliceStart.Item1; + double oneXUnit = gridWidth / xExtent; + return (oneXUnit * adjustedX); + } + + /// + /// Gets x-axis range. This would be the range of z-values for a given vertical slice + /// + /// + protected override Tuple GetXRange() + { + return new Tuple(Grid.MinZ, Grid.MaxZ); + } + + /// + /// Gets Y-axis range. This would range from 0 to the maximum depth for the vertical slice + /// + /// + protected override Tuple GetYRange() + { + return new Tuple(0, Profile.GetMaxY()); + } + + /// + /// Draws the depth profile for a point on the vertical slice + /// + /// + protected override void DrawSlice(DrawingContext drawingContext) + { + Profile.Grid = Grid; + + var profileXPosMu = CrossSection.SliceStart.Item1 + ((CrossSection.SliceEnd.Item1 - CrossSection.SliceStart.Item1) / 3); // This should be selectable by the user + var xIndex = DataGrid.ClosestTo(Grid.Columns, profileXPosMu); + Profile.Draw(xIndex, GridRect, drawingContext, CrossSection.Property); + + Point profileStartPoint = new Point( + GridRect.Left + ToXPosition(profileXPosMu, GridRect.Width), + GridRect.Top + ToYPosition(Grid.Rows.First(), GridRect.Height)); + Point profileEndPoint = new Point( + GridRect.Left + ToXPosition(profileXPosMu, GridRect.Width), + GridRect.Top + ToYPosition(Grid.Rows.Last(), GridRect.Height)); + } + } +} diff --git a/MapLinkProApp/DockManager.cs b/MapLinkProApp/DockManager.cs new file mode 100644 index 0000000..7f83be9 --- /dev/null +++ b/MapLinkProApp/DockManager.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MapLinkProApp; + +namespace MapLinkProApp +{ + public class DockManager + { + public System.Windows.Controls.DockPanel DockPanel { get; set; } + public enum DockLocation + { + Left, + Right, + Top, + Bottom + }; + public DockLocation Location { get; set; } + + public class GridLocation + { + public System.Windows.Controls.Grid Grid { get; set; } + public int X { get; set; } + public int Y { get; set; } + } + + public GridLocation MaximiseGridLocation { get; set; } + public GridLocation FloatGridLocation { get; set; } + + public System.Windows.Window MainWindow { get => System.Windows.Application.Current.MainWindow; } + + public DockableWindow Floating { get; private set; } + public DockableWindow Maximised { get; private set; } + private List Windows { get; } = new List(); + + public void AddWindow(DockableWindow window) + { + Windows.Add(window); + + window.DockEvent += (o, e) => + { + Dock((DockableWindow)o); + }; + window.MaximiseEvent += (o, e) => + { + Maximise((DockableWindow)o); + }; + window.MinimiseEvent += (o, e) => + { + Minimise((DockableWindow)o); + }; + window.RestoreEvent += (o, e) => + { + Float((DockableWindow)o); + }; + } + + private static void RemoveChild(DockableWindow window, System.Windows.Controls.UIElementCollection children) + { + if (window.ContentBackup == null) + { + return; + } + + int index = children.IndexOf(window.ContentBackup); + if (index < 0) + { + return; + } + + children.RemoveAt(index); + + window.Window.Content = window.ContentBackup; + } + + private void Undock(DockableWindow window) + { + RemoveChild(window, DockPanel.Children); + } + + private void RemoveFromGrid(DockableWindow window, System.Windows.Controls.Grid grid) + { + RemoveChild(window, grid.Children); + } + + private void FillGridCell(DockableWindow window, GridLocation cell) + { + if (cell == null) + { + throw new NullReferenceException("No grid cell"); + } + + window.Minimised.Visibility = System.Windows.Visibility.Hidden; + window.Window.Hide(); + + if (window.ContentBackup == null) + { + window.ContentBackup = (System.Windows.UIElement)window.Window.Content; + } + + window.Window.Content = null; + System.Windows.Controls.Grid.SetColumn(window.ContentBackup, cell.X); + System.Windows.Controls.Grid.SetRow(window.ContentBackup, cell.Y); + cell.Grid.Children.Add(window.ContentBackup); + } + + public void Float(DockableWindow window) + { + Undock(window); + + if (Floating != null) + { + Minimise(Floating); + } + Floating = null; + + if (Maximised != null) + { + Minimise(Maximised); + } + Maximised = null; + + if (FloatGridLocation == null) + { + throw new NullReferenceException("No float grid"); + } + + Floating = window; + FillGridCell(Floating, FloatGridLocation); + } + + public void Maximise(DockableWindow window) + { + Undock(window); + + if (Floating != null) + { + Minimise(Floating); + } + Floating = null; + + if (Maximised != null) + { + Minimise(Maximised); + } + + if (MaximiseGridLocation == null) + { + throw new NullReferenceException("No maximise grid"); + } + + Maximised = window; + FillGridCell(Maximised, MaximiseGridLocation); + } + + public void Minimise(DockableWindow window) + { + Undock(window); + RemoveFromGrid(window, MaximiseGridLocation.Grid); + RemoveFromGrid(window, FloatGridLocation.Grid); + + if (window == Maximised) + { + Maximised = null; + } + if (window == Floating) + { + Floating = null; + } + + window.Window.Hide(); + window.Minimised.Visibility = System.Windows.Visibility.Visible; + } + + public void Dock(DockableWindow window) + { + Undock(window); + RemoveFromGrid(window, MaximiseGridLocation.Grid); + RemoveFromGrid(window, FloatGridLocation.Grid); + + if (DockPanel == null) + { + throw new NullReferenceException("No dock panel"); + } + + if (window == Maximised) + { + Maximised = null; + } + if (window == Floating) + { + Floating = null; + } + + window.Minimised.Visibility = System.Windows.Visibility.Hidden; + window.Window.Hide(); + + if (window.ContentBackup == null) + { + window.ContentBackup = (System.Windows.UIElement)window.Window.Content; + } + window.Window.Content = null; + + DockPanel.Children.Add(window.ContentBackup); + } + } +} diff --git a/MapLinkProApp/DockableWindow.cs b/MapLinkProApp/DockableWindow.cs new file mode 100644 index 0000000..0ae2e01 --- /dev/null +++ b/MapLinkProApp/DockableWindow.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MapLinkProApp +{ + public class DockableWindowEventArgs + { + public enum EventType + { + Minimise, + Maximise, + Restore, + Dock + }; + public EventType Type { get; set; } + } + + public class DockableWindow + { + public System.Windows.Controls.Control Minimised { get; set; } + public System.Windows.Window Window { get; set; } + + public System.Windows.Window OwnerBackup { get; set; } + public System.Windows.UIElement ContentBackup { get; set; } + + public delegate void DockableWindowEvent(object sender, DockableWindowEventArgs e); + + public event DockableWindowEvent MaximiseEvent; + public event DockableWindowEvent MinimiseEvent; + public event DockableWindowEvent RestoreEvent; + public event DockableWindowEvent DockEvent; + + public void RaiseMaximiseEvent() + { + MaximiseEvent?.Invoke(this, new DockableWindowEventArgs { Type = DockableWindowEventArgs.EventType.Maximise }); + } + + public void RaiseMinimiseEvent() + { + MinimiseEvent?.Invoke(this, new DockableWindowEventArgs { Type = DockableWindowEventArgs.EventType.Minimise }); + } + + public void RaiseRestoreEvent() + { + RestoreEvent?.Invoke(this, new DockableWindowEventArgs { Type = DockableWindowEventArgs.EventType.Restore }); + } + + public void RaiseDockEvent() + { + DockEvent?.Invoke(this, new DockableWindowEventArgs { Type = DockableWindowEventArgs.EventType.Dock }); + } + } +} diff --git a/MapLinkProApp/LayerProperty.cs b/MapLinkProApp/LayerProperty.cs new file mode 100644 index 0000000..cd1208b --- /dev/null +++ b/MapLinkProApp/LayerProperty.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace MapLinkProApp +{ + internal class LayerProperty + { + public static readonly DependencyProperty LayerNameProperty = DependencyProperty.RegisterAttached( + "SetLayerName", typeof(string), typeof(LayerProperty), new PropertyMetadata() ); + + public static string GetLayerNameProperty(DependencyObject obj) + { + return (string)obj.GetValue(LayerNameProperty); + } + + public static void SetLayerNameProperty(DependencyObject obj, string value) + { + obj.SetValue(LayerNameProperty, value); + } + } +} diff --git a/MapLinkProApp/LayerSelector.cs b/MapLinkProApp/LayerSelector.cs new file mode 100644 index 0000000..cee688c --- /dev/null +++ b/MapLinkProApp/LayerSelector.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows; +using System.Windows.Media.Imaging; +using System.Windows.Media; +using System.ComponentModel; + +namespace MapLinkProApp +{ + internal class LayerSelector + { + private const String BORDER_PREFIX = "Border_"; + private const String BUTTON_PREFIX = "Button_"; + private const String LABEL_PREFIX = "Label_"; + private const String LAYER_OPTIONS_PREFIX = "LayerOption_"; + private const String LAYER_PREFIX = "Layer_"; + private const int TRANSPARENT_INDEX = 1; + private const int DUPLICATE_INDEX = 2; + + // Rpresents the layers to be added to the layer selector + public record struct LayerDetails + { + // Unique name of the layer, as configured in Maplink + public string Name { get; init; } + // Layer Property from the configuration + public string Label { get; init; } + // Index at which the layer is to be inserted (-1 for default) + public int Index { get; set; } + }; + + private Window parentWindow; + + // This stores the labels of layers that are switched on + private HashSet activeLayers = new HashSet(); + + public DrawingSurfacePanel.MapViewerPanel MapPanel { get; set; } + + public string SelectedLayer { get; set; } + + public event EventHandler LayerChanged; + + public LayerSelector(Window parentWindow) + { + this.parentWindow = parentWindow; + } + + /// + /// Returns the first active layer. This is useful in determining which depths to display in the depth selector + /// + /// + public string GetActiveLayer() + { + return activeLayers.Count() > 0 ? activeLayers.First().Name : null; + } + + /// + /// This removes a given layer from the layer selection panel. It also removes any displayed overlays from the map + /// prior to removing it fromthe selection panel + /// + /// + /// List of layer names to be removed + /// List of indexes from which the elements are removed. -1 if nothing is removed + public List RemoveLayerFromSelectionPanel(StackPanel layersPanel, List layers) + { + List removedIndexes = new List(); + layers.ForEach(layer => { + showLayerOnMap(false, layer.Name); + string layerId = GenerateIdFromLayerName(LAYER_PREFIX, layer.Name); + var result = FindChild(layersPanel, layerId); + + if (result.Item2 != null) + { + layersPanel.Children.Remove(result.Item2); + removedIndexes.Add(result.Item1); + } + else + { + throw new KeyNotFoundException("Layer " + layer.Name + " not found"); + } + }); + return removedIndexes; + } + + /// + /// This creates the layer dropdown and layer options controls and adds them to the layer selection panel + /// + /// + /// Dictionary that maps layer name to layer label + public void AddLayerToSelectionPanel(StackPanel layersPanel, List layers) + { + layers.ForEach(layer => { + Button layerButton = createLayerButton(layer); + // Add click event to it so that it can show layer options panel + layerButton.Name = GenerateIdFromLayerName(BUTTON_PREFIX, layer.Name); + layerButton.Click += new RoutedEventHandler(LayerButton_Click); + + StackPanel layerOptionsPanel = new StackPanel(); + layerOptionsPanel.Visibility = Visibility.Collapsed; + layerOptionsPanel.Orientation = Orientation.Vertical; + layerOptionsPanel.Name = GenerateIdFromLayerName(LAYER_OPTIONS_PREFIX, layer.Name); + + if(MapPanel != null) + { + // Transparency is set on a Map and we wouldn't be able to set transparency if we are not displaying the map + AddItemToLayerOption(layerOptionsPanel, "Transparent", layer.Name, TRANSPARENT_INDEX); + } + + layerOptionsPanel.Margin = new Thickness(5); + + Border layerOptionsBorder = new Border(); + layerOptionsBorder.Name = GenerateIdFromLayerName(BORDER_PREFIX, layer.Name); + Style borderStyle = parentWindow.FindResource("StackBorder") as Style; + layerOptionsBorder.Style = borderStyle; + layerOptionsBorder.Child = layerOptionsPanel; + + StackPanel layerPanel = new StackPanel(); + layerPanel.Orientation = Orientation.Vertical; + layerPanel.Margin = new Thickness(15, 0, 15, 10); + layerPanel.Name = GenerateIdFromLayerName(LAYER_PREFIX, layer.Name); + layerPanel.Children.Add(layerButton); + layerPanel.Children.Add(layerOptionsBorder); + // We set layername here so that it can be retrieved on event handling methods later + layerPanel.SetValue(LayerProperty.LayerNameProperty, layer.Name); + + if (layer.Index == -1) + { + layersPanel.Children.Add(layerPanel); + } + else + { + layersPanel.Children.Insert(layer.Index, layerPanel); + } + + // Switch on the layer if it was already selected to be displayed + if (activeLayers.Where(item => item.Label.Equals(layer.Label)).Count () > 0) + { + activateLayer(layer, (StackPanel)layerButton.Content); + } + }); + } + + /// + /// Sets the layer selection checkbox and switches on the corresponding layer on the Map + /// + /// Details of the layer to be turned on + /// Panel containing the layer checkbox + private void activateLayer(LayerDetails layer, StackPanel layerButtonPanel) + { + string layerId = GenerateIdFromLayerName("", layer.Name); + CheckBox layerCheckbox = FindChild(layerButtonPanel, layerId).Item2; + layerCheckbox.IsChecked = true; + + // Turn on the layer on the Map + showLayerOnMap(true, layer.Name); + } + + /// + /// This creates the layer dropdown control. The control consists of a checkbox, label and dropdown image added to a button + /// + /// This is used to name the control so that we can derive the layer name from control name at a later stage + /// layer dropdown control + private Button createLayerButton(LayerDetails layer) + { + string layerName = layer.Name; + CheckBox checkbox = new CheckBox(); + checkbox.VerticalAlignment = VerticalAlignment.Center; + checkbox.Margin = new Thickness(5); + checkbox.Name = GenerateIdFromLayerName("", layerName); + checkbox.AddHandler(CheckBox.ClickEvent, new RoutedEventHandler(LayerCheckBoxChanged)); + + Image icon = new Image + { + Height = 20, + Width = 20, + Source = new BitmapImage(new Uri("../img/drop-down.png", UriKind.Relative)), + VerticalAlignment = VerticalAlignment.Center + }; + Label label = new Label(); + label.Width = 150; + label.Name = GenerateIdFromLayerName(LABEL_PREFIX, layerName); + label.Content = layer.Label; + + StackPanel layerButtonPanel = new StackPanel(); + layerButtonPanel.Orientation = Orientation.Horizontal; + layerButtonPanel.VerticalAlignment = VerticalAlignment.Center; + layerButtonPanel.Children.Add(checkbox); + layerButtonPanel.Children.Add(label); + layerButtonPanel.Children.Add(icon); + + layerButtonPanel.SetValue(LayerProperty.LayerNameProperty, layerName); + + Button layerButton = new Button(); + Style style = parentWindow.FindResource("overlayButton") as Style; + layerButton.Style = style; + layerButton.Content = layerButtonPanel; + + return layerButton; + } + + /// + /// Adds the various checkboxes to the layer options panel + /// + /// Parent element to which the checkbox would be added + /// + /// Layer name is used to name the label so that we can derive the layername from element name at later stage + /// this is used to create distinct names for each checkbox + private void AddItemToLayerOption(StackPanel parentElement, string labelContent, string layerName, int index) + { + StackPanel stackPanel = new StackPanel(); + stackPanel.Orientation = Orientation.Horizontal; + stackPanel.Name = GenerateIdFromLayerName(LAYER_OPTIONS_PREFIX + index, layerName); + // The layer name is set here so that it can be retrieved later from event handling + // functions of its children + stackPanel.SetValue(LayerProperty.LayerNameProperty, layerName); + + CheckBox checkbox = new CheckBox(); + Label label = new Label(); + label.Name = LAYER_OPTIONS_PREFIX + index; + label.Content = labelContent; + + if (index == 1) + { + checkbox.AddHandler(CheckBox.ClickEvent, new RoutedEventHandler(TransparentCheckBoxChanged)); + } + else + { + checkbox.AddHandler(CheckBox.ClickEvent, new RoutedEventHandler(DuplicateCheckBoxChanged)); + } + + stackPanel.Children.Add(checkbox); + stackPanel.Children.Add(label); + + parentElement.Children.Add(stackPanel); + } + + /// + /// Event handler for layer dropdown, to expand it so that layer options are visible + /// + /// + /// + private void LayerButton_Click(object sender, RoutedEventArgs e) + { + Button button = (Button)sender; + StackPanel stackPanel = (StackPanel)button.Parent; + + string layerName = GetLayerNameFromProperty(stackPanel); + string layerOptionsName = GenerateIdFromLayerName(LAYER_OPTIONS_PREFIX, layerName); + + StackPanel layerOptionsPanel = FindChild(stackPanel, layerOptionsName).Item2; + Visibility visibility = layerOptionsPanel.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; + layerOptionsPanel.Visibility = visibility; + } + + /// + /// Checkbox event handler function for the transparency functionality + /// + /// + /// + private void TransparentCheckBoxChanged(object sender, RoutedEventArgs e) + { + CheckBox checkBox = (CheckBox)sender; + StackPanel parent = (StackPanel)checkBox.Parent; + + string layerName = GetLayerNameFromProperty(parent); + + string labelName = LAYER_OPTIONS_PREFIX + TRANSPARENT_INDEX; + Label label = FindChild