// Copyright (C) Stichting Deltares and State of the Netherlands 2024. All rights reserved.
//
// This file is part of Riskeer.
//
// Riskeer is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see .
//
// All names, logos, and references to "Deltares" are registered trademarks of
// Stichting Deltares and remain full property of Stichting Deltares at all times.
// All rights reserved.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Text;
using System.Linq;
using System.Windows.Forms;
using Core.Common.Base;
using Core.Common.Util.Drawing;
using Core.Components.DotSpatial.Forms.Properties;
using Core.Components.DotSpatial.Layer;
using Core.Components.DotSpatial.Layer.BruTile;
using Core.Components.DotSpatial.MapFunctions;
using Core.Components.Gis.Data;
using Core.Components.Gis.Exceptions;
using Core.Components.Gis.Forms;
using DotSpatial.Controls;
using DotSpatial.Data;
using DotSpatial.Projections;
using GeoAPI.Geometries;
using log4net;
using Timer = System.Timers.Timer;
namespace Core.Components.DotSpatial.Forms
{
///
/// This class describes a map control with configured projection and function mode.
///
public partial class MapControl : UserControl, IMapControl
{
private const int updateTimerInterval = 10;
private static readonly PrivateFontCollection privateFontCollection = new PrivateFontCollection();
private static readonly Font font = FontHelper.CreateFont(Resources.Symbols, privateFontCollection);
private readonly ILog log = LogManager.GetLogger(typeof(MapControl));
private readonly Cursor defaultCursor = Cursors.Default;
private readonly RecursiveObserver mapDataCollectionObserver;
private readonly Observer backGroundMapDataObserver;
private readonly List drawnMapDataList = new List();
private readonly MapControlBackgroundLayerStatus backgroundLayerStatus = new MapControlBackgroundLayerStatus();
private readonly List mapDataLayersToUpdate = new List();
private Map map;
private bool removing;
private MapFunctionSelectionZoom mapFunctionSelectionZoom;
private RdNewMouseCoordinatesMapExtension mouseCoordinatesMapExtension;
private MapDataCollection data;
private ImageBasedMapData backgroundMapData;
private Timer updateTimer;
///
/// Creates a new instance of .
///
public MapControl()
{
InitializeComponent();
SetFonts();
InitializeMap();
panToolStripButton.PerformClick();
showCoordinatesToolStripButton.PerformClick();
mapDataCollectionObserver = new RecursiveObserver(HandleMapDataCollectionChange, mdc => mdc.Collection);
backGroundMapDataObserver = new Observer(HandleBackgroundMapDataChange);
InitializeUpdateTimer();
}
public MapDataCollection Data
{
get
{
return data;
}
set
{
if (HasMapData)
{
ClearAllMapData(true);
}
data = value;
mapDataCollectionObserver.Observable = data;
if (HasMapData && !removing)
{
DrawInitialMapData();
}
}
}
public ImageBasedMapData BackgroundMapData
{
get
{
return backgroundMapData;
}
set
{
if (HasMapData)
{
ClearAllMapData(false);
}
backgroundMapData = value;
backGroundMapDataObserver.Observable = backgroundMapData;
if (HasMapData && !removing)
{
DrawInitialMapData();
}
}
}
public void RemoveAllData()
{
removing = true;
Data = null;
BackgroundMapData = null;
removing = false;
}
protected override void Dispose(bool disposing)
{
map.Dispose();
mouseCoordinatesMapExtension.Dispose();
mapDataCollectionObserver.Dispose();
backGroundMapDataObserver.Dispose();
backgroundLayerStatus.Dispose();
if (disposing)
{
components?.Dispose();
}
base.Dispose(disposing);
}
private ProjectionInfo Projection
{
get
{
return map.Projection;
}
set
{
ProjectionInfo oldProjectInfo = map.Projection;
map.Projection = value;
ReprojectViewExtents(oldProjectInfo, value);
}
}
private bool HasMapData
{
get
{
return backgroundMapData != null || data != null;
}
}
private void SetFonts()
{
panToolStripButton.Font = font;
zoomToRectangleToolStripButton.Font = font;
zoomToVisibleLayersToolStripButton.Font = font;
showCoordinatesToolStripButton.Font = font;
}
private void InitializeMap()
{
map = new DotSpatialMap
{
ProjectionModeDefine = ActionMode.Never,
Dock = DockStyle.Fill,
ZoomOutFartherThanMaxExtent = true,
Projection = MapDataConstants.FeatureBasedMapDataCoordinateSystem
};
MapFunctionPan mapFunctionPan = map.MapFunctions.OfType().First();
mapFunctionPan.FunctionActivated += MapFunctionActivateFunction;
mapFunctionPan.MouseDown += MapFunctionPanOnMouseDown;
mapFunctionPan.MouseUp += MapFunctionOnMouseUp;
mapFunctionSelectionZoom = new MapFunctionSelectionZoom(map);
map.MapFunctions.Add(mapFunctionSelectionZoom);
mapFunctionSelectionZoom.FunctionActivated += MapFunctionActivateFunction;
mapFunctionSelectionZoom.MouseDown += MapFunctionSelectionZoomOnMouseDown;
mapFunctionSelectionZoom.MouseUp += MapFunctionOnMouseUp;
mouseCoordinatesMapExtension = new RdNewMouseCoordinatesMapExtension(map);
tableLayoutPanel.Controls.Add(map, 0, 0);
}
private void ReprojectViewExtents(ProjectionInfo projectFrom, ProjectionInfo projectTo)
{
double[] viewExtentXY =
{
map.ViewExtents.MinX,
map.ViewExtents.MinY,
map.ViewExtents.MaxX,
map.ViewExtents.MaxY
};
double[] viewExtentZ =
{
0.0,
0.0
};
Reproject.ReprojectPoints(viewExtentXY, viewExtentZ, projectFrom, projectTo, 0, 2);
map.ViewExtents = new Extent(viewExtentXY);
}
private void ReprojectLayers(IEnumerable layersToReproject)
{
foreach (IMapLayer mapLayer in layersToReproject)
{
if (!mapLayer.Projection.Equals(Projection))
{
mapLayer.Reproject(Projection);
mapLayer.Invalidate();
}
}
}
#region Background layer
///
/// Attempts to initialize the background layer.
///
/// true if initialization of the background layer was successful,
/// false otherwise.
///
private bool InitializeBackgroundLayer()
{
BruTileLayer backgroundLayer;
try
{
backgroundLayer = ImageBasedMapDataLayerFactory.Create(backgroundMapData);
}
catch (ConfigurationInitializationException e)
{
if (!backgroundLayerStatus.PreviousBackgroundLayerCreationFailed)
{
string fullMessage = string.Format(Resources.MapControl_HandleBruTileInitializationException_Message_0_therefore_cannot_show_background_layer,
e.Message);
log.Error(fullMessage, e);
}
backgroundLayerStatus.LayerInitializationFailed();
return false;
}
if (backgroundLayer == null)
{
return false;
}
backgroundLayerStatus.LayerInitializationSuccessful(backgroundLayer, backgroundMapData);
return true;
}
private void InsertBackgroundLayer()
{
if (backgroundMapData.IsConfigured)
{
if (InitializeBackgroundLayer())
{
InsertBackgroundLayerAndReprojectExistingLayers();
}
}
else
{
Projection = MapDataConstants.FeatureBasedMapDataCoordinateSystem;
ReprojectLayers(map.Layers);
}
}
private void InsertBackgroundLayerAndReprojectExistingLayers()
{
IMapLayer[] existingMapLayers = map.Layers.ToArray();
Projection = backgroundLayerStatus.BackgroundLayer.Projection;
map.Layers.Insert(0, backgroundLayerStatus.BackgroundLayer);
ReprojectLayers(existingMapLayers);
}
private void HandleBackgroundMapDataChange()
{
if (backgroundLayerStatus.BackgroundLayer != null)
{
if (!backgroundLayerStatus.HasSameConfiguration(backgroundMapData))
{
map.Layers.Remove(backgroundLayerStatus.BackgroundLayer);
backgroundLayerStatus.ClearConfiguration();
InsertBackgroundLayer();
}
else
{
backgroundLayerStatus.BackgroundLayer.IsVisible = backgroundMapData.IsVisible;
backgroundLayerStatus.BackgroundLayer.Transparency = Convert.ToSingle(backgroundMapData.Transparency);
}
}
else
{
InsertBackgroundLayer();
}
}
#endregion
#region DrawMapData
///
/// Lookup class for administration related to drawn map data layers.
///
private class DrawnMapData
{
///
/// The feature based map data which the drawn is based upon.
///
public FeatureBasedMapData FeatureBasedMapData { get; set; }
///
/// The drawn map data layer.
///
public IFeatureBasedMapDataLayer FeatureBasedMapDataLayer { get; set; }
///
/// The observer attached to and responsible for updating .
///
public Observer Observer { get; set; }
}
private void ClearAllMapData(bool expectedRecreationOfBackgroundLayer)
{
foreach (DrawnMapData drawnMapData in drawnMapDataList)
{
drawnMapData.Observer.Dispose();
}
drawnMapDataList.Clear();
map.ClearLayers();
Projection = MapDataConstants.FeatureBasedMapDataCoordinateSystem;
backgroundLayerStatus?.ClearConfiguration(expectedRecreationOfBackgroundLayer);
}
private void DrawInitialMapData()
{
if (backgroundMapData != null && backgroundMapData.IsConfigured && InitializeBackgroundLayer())
{
Projection = backgroundLayerStatus.BackgroundLayer.Projection;
map.Layers.Add(backgroundLayerStatus.BackgroundLayer);
}
if (Data != null)
{
foreach (FeatureBasedMapData featureBasedMapData in Data.GetFeatureBasedMapDataRecursively())
{
DrawMapData(featureBasedMapData);
}
}
}
private void DrawMapData(FeatureBasedMapData featureBasedMapData)
{
IFeatureBasedMapDataLayer featureBasedMapDataLayer = FeatureBasedMapDataLayerFactory.Create(featureBasedMapData);
var drawnMapData = new DrawnMapData
{
FeatureBasedMapData = featureBasedMapData,
FeatureBasedMapDataLayer = featureBasedMapDataLayer
};
drawnMapData.Observer = new Observer(() =>
{
mapDataLayersToUpdate.Add(drawnMapData.FeatureBasedMapDataLayer);
StartUpdateTimer();
})
{
Observable = featureBasedMapData
};
drawnMapDataList.Add(drawnMapData);
if (!Projection.Equals(featureBasedMapDataLayer.Projection))
{
featureBasedMapDataLayer.Reproject(Projection);
}
map.Layers.Add(featureBasedMapDataLayer);
}
private void DrawMissingMapDataOnCollectionChange(IEnumerable mapDataThatShouldBeDrawn,
IDictionary drawnMapDataLookup)
{
foreach (FeatureBasedMapData mapDataToDraw in mapDataThatShouldBeDrawn.Where(mapDataToDraw => !drawnMapDataLookup.ContainsKey(mapDataToDraw)))
{
DrawMapData(mapDataToDraw);
}
}
private void HandleMapDataCollectionChange()
{
List mapDataThatShouldBeDrawn = Data.GetFeatureBasedMapDataRecursively().ToList();
Dictionary drawnMapDataLookup = drawnMapDataList.ToDictionary(dmd => dmd.FeatureBasedMapData, dmd => dmd);
DrawMissingMapDataOnCollectionChange(mapDataThatShouldBeDrawn, drawnMapDataLookup);
RemoveRedundantMapDataOnCollectionChange(mapDataThatShouldBeDrawn, drawnMapDataLookup);
drawnMapDataLookup = drawnMapDataList.ToDictionary(dmd => dmd.FeatureBasedMapData, dmd => dmd);
ReorderMapDataOnCollectionChange(mapDataThatShouldBeDrawn, drawnMapDataLookup);
}
private void ReorderMapDataOnCollectionChange(IList mapDataThatShouldBeDrawn,
IDictionary drawnMapDataLookup)
{
int shiftedIndex = backgroundLayerStatus.BackgroundLayer != null ? 1 : 0;
for (var i = 0; i < mapDataThatShouldBeDrawn.Count; i++)
{
map.Layers.Move(drawnMapDataLookup[mapDataThatShouldBeDrawn[i]].FeatureBasedMapDataLayer, i + shiftedIndex);
}
}
private void RemoveMapData(DrawnMapData drawnMapDataToRemove)
{
drawnMapDataToRemove.Observer.Dispose();
drawnMapDataList.Remove(drawnMapDataToRemove);
map.Layers.Remove(drawnMapDataToRemove.FeatureBasedMapDataLayer);
}
private void RemoveRedundantMapDataOnCollectionChange(IEnumerable mapDataThatShouldBeDrawn,
IDictionary drawnMapDataLookup)
{
foreach (FeatureBasedMapData featureBasedMapData in drawnMapDataLookup.Keys.Except(mapDataThatShouldBeDrawn))
{
RemoveMapData(drawnMapDataLookup[featureBasedMapData]);
}
}
#endregion
#region Map Interaction
public void ZoomToVisibleLayers()
{
ZoomToVisibleLayers(Data);
}
public void ZoomToVisibleLayers(MapData mapData)
{
Envelope envelope = CreateEnvelopeForVisibleLayers(mapData);
if (!envelope.IsNull)
{
Extent extent = envelope.ToExtent();
AddPadding(extent);
map.ViewExtents = extent;
}
}
private static void AddPadding(Extent extent)
{
double padding = Math.Min(extent.Height, extent.Width) * 0.05;
if (Math.Max(extent.Height, extent.Width) + padding <= double.MaxValue)
{
extent.ExpandBy(padding);
}
}
///
/// Defines the area taken up by the visible map-data based on the provided map-data.
///
/// The data to determine the visible extent for.
/// The area definition.
/// Thrown when is
/// not part of the drawn map features.
private Envelope CreateEnvelopeForVisibleLayers(MapData mapData)
{
if (mapData is MapDataCollection collection)
{
return CreateEnvelopeForVisibleLayers(collection);
}
DrawnMapData drawnMapData = drawnMapDataList.FirstOrDefault(dmd => dmd.FeatureBasedMapData.Equals(mapData));
if (drawnMapData == null)
{
throw new ArgumentException($@"Can only zoom to {nameof(MapData)} that is part of this {nameof(MapControl)}s drawn {nameof(mapData)}.",
nameof(mapData));
}
var envelope = new Envelope();
if (LayerHasVisibleExtent(drawnMapData.FeatureBasedMapDataLayer))
{
envelope.ExpandToInclude(drawnMapData.FeatureBasedMapDataLayer.Extent.ToEnvelope());
}
return envelope;
}
///
/// Defines the area taken up by the visible map-data based on the provided map-data.
///
/// The data to determine the visible extent for.
/// The area definition.
/// Thrown when or
/// any of its children is not part of the drawn map features.
private Envelope CreateEnvelopeForVisibleLayers(MapDataCollection mapData)
{
var envelope = new Envelope();
foreach (MapData childMapData in mapData.Collection)
{
envelope.ExpandToInclude(CreateEnvelopeForVisibleLayers(childMapData));
}
return envelope;
}
private static bool LayerHasVisibleExtent(IMapLayer layer)
{
return layer.IsVisible && !layer.Extent.IsEmpty();
}
private void MapFunctionActivateFunction(object sender, EventArgs e)
{
map.Cursor = defaultCursor;
}
private void MapFunctionOnMouseUp(object sender, GeoMouseArgs e)
{
map.Cursor = defaultCursor;
}
private void MapFunctionPanOnMouseDown(object sender, GeoMouseArgs geoMouseArgs)
{
map.Cursor = geoMouseArgs.Button != MouseButtons.Right ? Cursors.Hand : defaultCursor;
}
private void MapFunctionSelectionZoomOnMouseDown(object sender, GeoMouseArgs geoMouseArgs)
{
switch (geoMouseArgs.Button)
{
case MouseButtons.Left:
map.Cursor = Cursors.SizeNWSE;
break;
default:
map.Cursor = map.IsBusy
? Cursors.SizeNWSE
: defaultCursor;
break;
}
}
private void PanToolStripButtonClick(object sender, EventArgs e)
{
if (panToolStripButton.Checked)
{
return;
}
map.FunctionMode = FunctionMode.Pan;
panToolStripButton.Checked = true;
zoomToRectangleToolStripButton.Checked = false;
}
private void ZoomToRectangleToolStripButtonClick(object sender, EventArgs e)
{
if (zoomToRectangleToolStripButton.Checked)
{
return;
}
map.FunctionMode = FunctionMode.None;
map.ActivateMapFunction(mapFunctionSelectionZoom);
panToolStripButton.Checked = false;
zoomToRectangleToolStripButton.Checked = true;
}
private void ZoomToVisibleLayersToolStripButtonClick(object sender, EventArgs e)
{
ZoomToVisibleLayers();
}
private void ShowCoordinatesToolStripButtonClick(object sender, EventArgs e)
{
showCoordinatesToolStripButton.Checked = !showCoordinatesToolStripButton.Checked;
if (showCoordinatesToolStripButton.Checked)
{
mouseCoordinatesMapExtension.Activate();
}
else
{
mouseCoordinatesMapExtension.Deactivate();
}
}
#endregion
#region Update timer
private void InitializeUpdateTimer()
{
updateTimer = new Timer
{
Interval = updateTimerInterval,
SynchronizingObject = this
};
updateTimer.Elapsed += (sender, args) =>
{
updateTimer.Stop();
UpdateMapDataLayers();
};
}
private void StartUpdateTimer()
{
if (updateTimer.Enabled)
{
updateTimer.Stop();
}
updateTimer.Start();
}
private void UpdateMapDataLayers()
{
foreach (IFeatureBasedMapDataLayer mapDataLayerToUpdate in mapDataLayersToUpdate.Distinct())
{
mapDataLayerToUpdate.Update();
}
mapDataLayersToUpdate.Clear();
}
#endregion
}
}