diff --git a/AnnoDesigner.Core/DataStructures/QuadTree.cs b/AnnoDesigner.Core/DataStructures/QuadTree.cs
index 0c6d10cb..493a7e3b 100644
--- a/AnnoDesigner.Core/DataStructures/QuadTree.cs
+++ b/AnnoDesigner.Core/DataStructures/QuadTree.cs
@@ -401,6 +401,15 @@ public QuadTree(Rect extent)
root = new Quadrant(extent);
}
+ ///
+ /// Create a
+ ///
+ /// The bounds of the
+ public QuadTree(Rect extent, IEnumerable enumerable) : this(extent)
+ {
+ AddRange(enumerable);
+ }
+
///
/// Insert a item into the QuadTree.
///
diff --git a/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs b/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs
index f2023daf..45ea9b0d 100644
--- a/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs
+++ b/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs
@@ -115,5 +115,33 @@ public static bool IsIgnoredObject(this AnnoObject annoObject)
{
return string.Equals(annoObject.Template, "Blocker", StringComparison.OrdinalIgnoreCase);
}
+
+ public static T MinOrDefault(this IEnumerable enumerable, T @default = default)
+ {
+ if (!enumerable.Any()) return @default;
+
+ return enumerable.Min();
+ }
+
+ public static TResult MinOrDefault(this IEnumerable enumerable, Func selector, TResult @default = default)
+ {
+ if (!enumerable.Any()) return @default;
+
+ return enumerable.Min(selector);
+ }
+
+ public static T MaxOrDefault(this IEnumerable enumerable, T @default = default)
+ {
+ if (!enumerable.Any()) return @default;
+
+ return enumerable.Max();
+ }
+
+ public static TResult MaxOrDefault(this IEnumerable enumerable, Func selector, TResult @default = default)
+ {
+ if (!enumerable.Any()) return @default;
+
+ return enumerable.Max(selector);
+ }
}
}
diff --git a/AnnoDesigner.Core/Layout/LayoutLoader.cs b/AnnoDesigner.Core/Layout/LayoutLoader.cs
index 79e80cee..a54e7bc8 100644
--- a/AnnoDesigner.Core/Layout/LayoutLoader.cs
+++ b/AnnoDesigner.Core/Layout/LayoutLoader.cs
@@ -64,6 +64,17 @@ public LayoutFile LoadLayout(Stream streamWithLayout, bool forceLoad = false)
return Load(jsonString, forceLoad);
}
+ public async Task LoadLayoutAsync(Stream streamWithLayout, bool forceLoad = false)
+ {
+ if (streamWithLayout == null)
+ {
+ throw new ArgumentNullException(nameof(streamWithLayout));
+ }
+ using var sr = new StreamReader(streamWithLayout);
+ var jsonString = await sr.ReadToEndAsync();
+ return await LoadAsync(jsonString, forceLoad);
+ }
+
private LayoutFile Load(string jsonString, bool forceLoad)
{
var layoutVersion = new LayoutFileVersionContainer() { FileVersion = 0 };
@@ -88,5 +99,10 @@ private LayoutFile Load(string jsonString, bool forceLoad)
_ => throw new NotImplementedException()
};
}
+
+ private Task LoadAsync(string jsonString, bool forceLoad)
+ {
+ return Task.Run(() => Load(jsonString, forceLoad));
+ }
}
}
diff --git a/AnnoDesigner.Core/Layout/Models/LayoutFile.cs b/AnnoDesigner.Core/Layout/Models/LayoutFile.cs
index e3b7fd02..475a163b 100644
--- a/AnnoDesigner.Core/Layout/Models/LayoutFile.cs
+++ b/AnnoDesigner.Core/Layout/Models/LayoutFile.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using AnnoDesigner.Core.Models;
@@ -21,5 +22,13 @@ public LayoutFile(IEnumerable objects)
FileVersion = CoreConstants.LayoutFileVersion;
Objects = objects.ToList();
}
+
+ public LayoutFile(LayoutFile copy)
+ {
+ FileVersion = copy.FileVersion;
+ LayoutVersion = (Version)copy.LayoutVersion.Clone();
+ Modified = copy.Modified;
+ Objects = copy.Objects.Select(x => new AnnoObject(x)).ToList();
+ }
}
}
\ No newline at end of file
diff --git a/AnnoDesigner.Core/Layout/Presets/IPresetLayout.cs b/AnnoDesigner.Core/Layout/Presets/IPresetLayout.cs
new file mode 100644
index 00000000..790c7cc6
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/IPresetLayout.cs
@@ -0,0 +1,7 @@
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ public interface IPresetLayout
+ {
+ public MultilangInfo Name { get; }
+ }
+}
diff --git a/AnnoDesigner.Core/Layout/Presets/LayoutPresetInfo.cs b/AnnoDesigner.Core/Layout/Presets/LayoutPresetInfo.cs
new file mode 100644
index 00000000..2c4c73e4
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/LayoutPresetInfo.cs
@@ -0,0 +1,20 @@
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ public class LayoutPresetInfo
+ {
+ public MultilangInfo Name { get; set; }
+
+ public MultilangInfo Description { get; set; }
+
+ public string Author { get; set; }
+
+ public string AuthorContact { get; set; }
+
+ public LayoutPresetInfo() { }
+
+ public LayoutPresetInfo(string name)
+ {
+ Name = name;
+ }
+ }
+}
diff --git a/AnnoDesigner.Core/Layout/Presets/MultilangInfo.cs b/AnnoDesigner.Core/Layout/Presets/MultilangInfo.cs
new file mode 100644
index 00000000..53a5390c
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/MultilangInfo.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ [JsonConverter(typeof(MultilangInfoConverter))]
+ public class MultilangInfo
+ {
+ private class MultilangInfoConverter : JsonConverter
+ {
+ public override MultilangInfo ReadJson(JsonReader reader, Type objectType, MultilangInfo existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonToken.String:
+ return reader.Value as string;
+ case JsonToken.StartObject:
+ return serializer.Deserialize>(reader);
+ default:
+ throw new JsonSerializationException($"Unexpected token during deserialization of {nameof(MultilangInfo)}");
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, MultilangInfo value, JsonSerializer serializer)
+ {
+ serializer.Serialize(writer, (object)value.Default ?? value.Translations);
+ }
+ }
+
+ private Dictionary Translations { get; set; }
+
+ private string Default { get; set; }
+
+ public string this[string language]
+ {
+ set
+ {
+ Translations ??= new Dictionary();
+ Translations[language] = value;
+ }
+ }
+
+ public string Translate(string language)
+ {
+ return Default ?? (
+ Translations.TryGetValue(language, out var translation)
+ ? translation
+ : Translations.Count > 0
+ ? $"{Translations.FirstOrDefault().Value} ({Translations.FirstOrDefault().Key})"
+ : string.Empty
+ );
+ }
+
+ public static implicit operator MultilangInfo(string value)
+ {
+ return new MultilangInfo()
+ {
+ Default = value
+ };
+ }
+
+ public static implicit operator MultilangInfo(Dictionary value)
+ {
+ return new MultilangInfo()
+ {
+ Translations = value
+ };
+ }
+
+ public static explicit operator string(MultilangInfo info)
+ {
+ var first = info.Translations.FirstOrDefault();
+ return info.Default ?? (info.Translations.Count > 0 ? $"{first.Value} ({first.Key})" : string.Empty);
+ }
+ }
+}
diff --git a/AnnoDesigner.Core/Layout/Presets/PresetLayout.cs b/AnnoDesigner.Core/Layout/Presets/PresetLayout.cs
new file mode 100644
index 00000000..7cd0da93
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/PresetLayout.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using AnnoDesigner.Core.Layout.Models;
+using AnnoDesigner.Core.Models;
+using Newtonsoft.Json;
+
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ public class PresetLayout : Notify, IPresetLayout, IDisposable
+ {
+ private bool disposed;
+ private List images;
+ private IFileSystem fileSystem;
+ private Func> renderLayoutToImage;
+
+ public MultilangInfo Name => Info.Name;
+
+ public LayoutPresetInfo Info { get; set; }
+
+ public string Author { get; set; }
+
+ public string AuthorContact { get; set; }
+
+ public LayoutFile Layout { get; set; }
+
+ public List Images
+ {
+ get { return images; }
+ set { UpdateProperty(ref images, value); }
+ }
+
+ private ZipArchive ZipArchive { get; set; }
+
+ [Obsolete($"Constructor should not be used to construct {nameof(PresetLayout)}. Use {nameof(OpenAsync)} instead.")]
+ public PresetLayout() { }
+
+ public static async Task OpenAsync(string zipFile, Func> renderLayoutToImage = null, IFileSystem fileSystem = null)
+ {
+ fileSystem ??= new FileSystem();
+ var zip = ZipFile.OpenRead(zipFile);
+
+ using var layoutFile = zip.GetEntry("layout.ad").Open();
+ var layout = await new LayoutLoader().LoadLayoutAsync(layoutFile, true).ConfigureAwait(false);
+ if (layout == null)
+ {
+ throw new ArgumentException("Provided ZIP file doesn't contain valid AD layout file with name layout.ad");
+ }
+
+ using var infoFile = zip.GetEntry("info.json")?.Open() ?? Stream.Null;
+ using var infoStream = new StreamReader(infoFile);
+ var info = JsonConvert.DeserializeObject(await infoStream.ReadToEndAsync().ConfigureAwait(false))
+ ?? new LayoutPresetInfo(fileSystem.Path.GetFileNameWithoutExtension(zipFile));
+
+ return new PresetLayout()
+ {
+ Info = info,
+ Layout = layout,
+ ZipArchive = zip,
+ fileSystem = fileSystem,
+ renderLayoutToImage = renderLayoutToImage
+ };
+ }
+
+ private bool IsImage(ZipArchiveEntry f)
+ {
+ switch (fileSystem.Path.GetExtension(f.FullName).ToLowerInvariant())
+ {
+ case ".png":
+ case ".jpg":
+ case ".jpeg":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public async Task LoadImages(Func> renderLayoutToImage = null)
+ {
+ renderLayoutToImage ??= this.renderLayoutToImage;
+
+ if (renderLayoutToImage == null)
+ {
+ throw new ArgumentNullException(nameof(renderLayoutToImage), $"Argument not provided nor set in {nameof(OpenAsync)}");
+ }
+
+ var images = new List
+ {
+ await renderLayoutToImage(Layout)
+ };
+
+ await Task.WhenAll(ZipArchive.Entries.Where(IsImage).Select(async f =>
+ {
+ using var stream = f.Open();
+ var imageStream = new MemoryStream();
+ await stream.CopyToAsync(imageStream).ConfigureAwait(true);
+
+ var image = new BitmapImage();
+ image.BeginInit();
+ image.StreamSource = imageStream;
+ image.EndInit();
+ image.Freeze();
+ images.Add(image);
+ }));
+
+ Images = images;
+ }
+
+ public void UnloadImages()
+ {
+ Images = null;
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposed)
+ {
+ if (disposing)
+ {
+ ZipArchive.Dispose();
+ ZipArchive = null;
+ }
+
+ disposed = true;
+ }
+ }
+ }
+}
diff --git a/AnnoDesigner.Core/Layout/Presets/PresetLayoutDirectory.cs b/AnnoDesigner.Core/Layout/Presets/PresetLayoutDirectory.cs
new file mode 100644
index 00000000..ac990162
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/PresetLayoutDirectory.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ public class PresetLayoutDirectory : IPresetLayout
+ {
+ public MultilangInfo Name { get; set; }
+
+ public List Presets { get; set; }
+ }
+}
diff --git a/AnnoDesigner.Core/Layout/Presets/PresetLayoutLoader.cs b/AnnoDesigner.Core/Layout/Presets/PresetLayoutLoader.cs
new file mode 100644
index 00000000..99c3ef49
--- /dev/null
+++ b/AnnoDesigner.Core/Layout/Presets/PresetLayoutLoader.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Media;
+using AnnoDesigner.Core.Layout.Models;
+using NLog;
+
+namespace AnnoDesigner.Core.Layout.Presets
+{
+ public class PresetLayoutLoader
+ {
+ private readonly IFileSystem _fileSystem;
+ private static readonly Logger Logger = LogManager.GetLogger(nameof(PresetLayoutLoader));
+
+ public Func> RenderLayoutToImage { get; set; }
+ public PresetLayoutLoader(Func> renderLayoutToImage, IFileSystem fileSystem = null)
+ {
+ RenderLayoutToImage = renderLayoutToImage;
+ _fileSystem = fileSystem ?? new FileSystem();
+ }
+
+ public async Task> LoadAsync(string rootDirectory)
+ {
+ var data = await LoadDirectoryAsync(rootDirectory).ConfigureAwait(false);
+
+ return data.Presets;
+ }
+
+ private async Task LoadDirectoryAsync(string directory)
+ {
+ var subdirectories = await Task.WhenAll(_fileSystem.Directory.GetDirectories(directory).Select(LoadDirectoryAsync)).ConfigureAwait(false);
+ var layouts = await Task.WhenAll(_fileSystem.Directory.GetFiles(directory, "*.zip").Select(LoadLayoutAsync)).ConfigureAwait(false);
+
+ return new PresetLayoutDirectory()
+ {
+ Name = _fileSystem.Path.GetFileName(directory),
+ Presets = subdirectories.Cast().Concat(layouts.Where(f => f != null)).ToList()
+ };
+ }
+
+ private async Task LoadLayoutAsync(string file)
+ {
+ try
+ {
+ return await PresetLayout.OpenAsync(file, RenderLayoutToImage, _fileSystem).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Logger.Warn(e, $"Failed to parse loadout preset {file}");
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/AnnoDesigner.Core/Models/IAppSettings.cs b/AnnoDesigner.Core/Models/IAppSettings.cs
index 005e1db3..4a7b3b25 100644
--- a/AnnoDesigner.Core/Models/IAppSettings.cs
+++ b/AnnoDesigner.Core/Models/IAppSettings.cs
@@ -56,5 +56,6 @@ public interface IAppSettings
bool InvertScrollingDirection { get; set; }
bool ShowScrollbars { get; set; }
bool IncludeRoadsInStatisticCalculation { get; set; }
+ string PresetLayoutLocation { get; set; }
}
}
diff --git a/AnnoDesigner/AnnoCanvas.xaml.cs b/AnnoDesigner/AnnoCanvas.xaml.cs
index a1105a6e..701a36ce 100644
--- a/AnnoDesigner/AnnoCanvas.xaml.cs
+++ b/AnnoDesigner/AnnoCanvas.xaml.cs
@@ -755,6 +755,14 @@ public AnnoCanvas(BuildingPresets presetsToUse,
StatisticsUpdated?.Invoke(this, UpdateStatisticsEventArgs.All);
}
+ public void Uninitialize()
+ {
+ _appSettings.SettingsChanged -= AppSettings_SettingsChanged;
+
+ SelectedObjects.Clear();
+ PlacedObjects.Clear();
+ }
+
#endregion
private bool _showScrollBars;
diff --git a/AnnoDesigner/AnnoDesigner.csproj b/AnnoDesigner/AnnoDesigner.csproj
index c8685b16..2139ea25 100644
--- a/AnnoDesigner/AnnoDesigner.csproj
+++ b/AnnoDesigner/AnnoDesigner.csproj
@@ -56,6 +56,18 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ Always
+
+
+ Always
+
SettingsSingleFileGenerator
Settings.Designer.cs
@@ -83,6 +95,7 @@
+
diff --git a/AnnoDesigner/Constants.cs b/AnnoDesigner/Constants.cs
index fc65bb50..ebdf8469 100644
--- a/AnnoDesigner/Constants.cs
+++ b/AnnoDesigner/Constants.cs
@@ -108,5 +108,10 @@ public static class Constants
/// The default number of recent files to show.
///
public const int MaxRecentFiles = 10;
+
+ ///
+ /// Default location of preset layout folder.
+ ///
+ public const string DefaultPresetLayoutLocation = ".\\Layouts";
}
}
diff --git a/AnnoDesigner/Converters/ReferenceToBooleanConverter.cs b/AnnoDesigner/Converters/ReferenceToBooleanConverter.cs
new file mode 100644
index 00000000..5f711fa1
--- /dev/null
+++ b/AnnoDesigner/Converters/ReferenceToBooleanConverter.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace AnnoDesigner.Converters
+{
+ [ValueConversion(typeof(object), typeof(object))]
+ public class ReferenceToValueConverter : IValueConverter
+ {
+ public object NullValue { get; set; }
+
+ public object NotNullValue { get; set; }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value == null ? NullValue : NotNullValue;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/AnnoDesigner/DataIO.cs b/AnnoDesigner/FrameworkElementExtensions.cs
similarity index 57%
rename from AnnoDesigner/DataIO.cs
rename to AnnoDesigner/FrameworkElementExtensions.cs
index 9478db85..2d170b9b 100644
--- a/AnnoDesigner/DataIO.cs
+++ b/AnnoDesigner/FrameworkElementExtensions.cs
@@ -3,26 +3,35 @@
using System.Windows.Media;
using System.Windows.Media.Imaging;
-namespace AnnoDesigner
+namespace AnnoDesigner.Core.Extensions
{
///
- /// Provides I/O methods
+ /// Provides extension methods for framework elements to render it to multiple targets.
///
- public static class DataIO
+ public static class FrameworkElementExtensions
{
- #region Render to file
-
///
- /// Renders the given target to an image file, png encoded.
+ /// Renders the given target to a bitmap.
///
/// target to be rendered
- /// output filename
- public static void RenderToFile(FrameworkElement target, string filename)
+ /// Bitmap containing rendered framework element
+ public static RenderTargetBitmap RenderToBitmap(this FrameworkElement target)
{
- // render control
const int dpi = 96;
var rtb = new RenderTargetBitmap((int)target.ActualWidth, (int)target.ActualHeight, dpi, dpi, PixelFormats.Default);
rtb.Render(target);
+
+ return rtb;
+ }
+
+ ///
+ /// Renders the given target to an image file, png encoded.
+ ///
+ /// target to be rendered
+ /// output filename
+ public static void RenderToFile(this FrameworkElement target, string filename)
+ {
+ var rtb = target.RenderToBitmap();
// put result into bitmap
var encoder = Constants.GetExportImageEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
@@ -32,7 +41,5 @@ public static void RenderToFile(FrameworkElement target, string filename)
encoder.Save(file);
}
}
-
- #endregion
}
}
\ No newline at end of file
diff --git a/AnnoDesigner/ImageWindow.xaml b/AnnoDesigner/ImageWindow.xaml
new file mode 100644
index 00000000..c5633525
--- /dev/null
+++ b/AnnoDesigner/ImageWindow.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/AnnoDesigner/ImageWindow.xaml.cs b/AnnoDesigner/ImageWindow.xaml.cs
new file mode 100644
index 00000000..03521afd
--- /dev/null
+++ b/AnnoDesigner/ImageWindow.xaml.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace AnnoDesigner
+{
+ ///
+ /// Interaction logic for ImageWindow.xaml
+ ///
+ public partial class ImageWindow : Window
+ {
+ public ImageWindow()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/AnnoDesigner/Layouts/Arctic/Medium Islands/colony03_a01_01.zip b/AnnoDesigner/Layouts/Arctic/Medium Islands/colony03_a01_01.zip
new file mode 100644
index 00000000..babeb22a
Binary files /dev/null and b/AnnoDesigner/Layouts/Arctic/Medium Islands/colony03_a01_01.zip differ
diff --git a/AnnoDesigner/Layouts/Arctic/Plateaus/colony03_a02_01.zip b/AnnoDesigner/Layouts/Arctic/Plateaus/colony03_a02_01.zip
new file mode 100644
index 00000000..d18d2014
Binary files /dev/null and b/AnnoDesigner/Layouts/Arctic/Plateaus/colony03_a02_01.zip differ
diff --git a/AnnoDesigner/Layouts/LayoutInfoSchema.json b/AnnoDesigner/Layouts/LayoutInfoSchema.json
new file mode 100644
index 00000000..068488f2
--- /dev/null
+++ b/AnnoDesigner/Layouts/LayoutInfoSchema.json
@@ -0,0 +1,54 @@
+{
+ "$schema": "https://json-schema.org/draft-04/schema",
+ "type": "object",
+ "definitions": {
+ "multilang": {
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "Single value used for all languages"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "eng": {
+ "type": "string"
+ },
+ "ger": {
+ "type": "string"
+ },
+ "fra": {
+ "type": "string"
+ },
+ "esp": {
+ "type": "string"
+ },
+ "pol": {
+ "type": "string"
+ },
+ "rus": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "properties": {
+ "Name": {
+ "#ref": "/definitions/multilang"
+ },
+ "Description": {
+ "#ref": "/definitions/multilang"
+ },
+ "Author": {
+ "type": "string"
+ },
+ "AuthorContact": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "Name"
+ ]
+}
\ No newline at end of file
diff --git a/AnnoDesigner/Layouts/Readme.md b/AnnoDesigner/Layouts/Readme.md
new file mode 100644
index 00000000..bde70c90
--- /dev/null
+++ b/AnnoDesigner/Layouts/Readme.md
@@ -0,0 +1,14 @@
+Each ZIP file in this folder and all subfolders should contain information about layout preset
+
+Layout preset must have:
+- layout file named layout.ad
+
+Layout preset can have:
+- info file names info.json
+ - JSON schema of this file is defined in LayoutInfoSchema.json in this folder
+ - if not provided the layout preset's name will be the ZIP file name
+- any number of image files which should be associated with that layout
+ - supported file types are "png", "jpg" and "jpeg" (case insensitive)
+
+ZIP files which fail to load as layout preset are ignored and not shown in AnnoDesigner
+Info about ZIP file failing to load is writen to the application log
diff --git a/AnnoDesigner/Localization/Localization.cs b/AnnoDesigner/Localization/Localization.cs
index 60cf4386..40b8ffee 100644
--- a/AnnoDesigner/Localization/Localization.cs
+++ b/AnnoDesigner/Localization/Localization.cs
@@ -36,7 +36,7 @@ private Localization() { }
private static IDictionary> TranslationsRaw { get; set; }
- private string SelectedLanguageCode => _commons.CurrentLanguageCode;
+ public string SelectedLanguageCode => _commons.CurrentLanguageCode;
public static IDictionary Translations => TranslationsRaw[Instance.SelectedLanguageCode];
@@ -1283,6 +1283,7 @@ private void Commons_SelectedLanguageChanged(object sender, EventArgs e)
{
OnPropertyChanged(nameof(Translations));
OnPropertyChanged(nameof(InstanceTranslations));
+ OnPropertyChanged(nameof(SelectedLanguageCode));
}
public string GetLocalization(string valueToTranslate)
diff --git a/AnnoDesigner/Localization/LocalizeBinding.cs b/AnnoDesigner/Localization/LocalizeBinding.cs
index 528fb51c..9ca89da7 100644
--- a/AnnoDesigner/Localization/LocalizeBinding.cs
+++ b/AnnoDesigner/Localization/LocalizeBinding.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Windows;
using System.Windows.Data;
+using AnnoDesigner.Core.Layout.Presets;
namespace AnnoDesigner.Localization
{
@@ -92,4 +94,61 @@ public DynamicLocalize(string keyPath) : this()
KeyPath = keyPath;
}
}
+
+ public class Multilang : MultiBinding
+ {
+ private class MultilangInfoConverter : IMultiValueConverter
+ {
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length == 2)
+ {
+ if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
+ {
+ return DependencyProperty.UnsetValue;
+ }
+
+ if (values[1] is null)
+ {
+ return null;
+ }
+
+ if (values[0] is string language && values[1] is MultilangInfo translations)
+ {
+ return translations.Translate(language);
+ }
+ }
+ throw new Exception($"Incorrect Multilang parameters. {values[1]}");
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private static MultilangInfoConverter SingletonConverter { get; } = new MultilangInfoConverter();
+
+ public string Path
+ {
+ set
+ {
+ Bindings.Add(new Binding(value));
+ }
+ }
+
+ public Multilang()
+ {
+ Bindings.Add(new Binding(nameof(Localization.Instance.SelectedLanguageCode))
+ {
+ Source = Localization.Instance
+ });
+ Converter = SingletonConverter;
+ }
+
+ public Multilang(string path) : this()
+ {
+ Path = path;
+ }
+ }
}
diff --git a/AnnoDesigner/MainWindow.xaml b/AnnoDesigner/MainWindow.xaml
index 6849678e..bcdb3c8e 100644
--- a/AnnoDesigner/MainWindow.xaml
+++ b/AnnoDesigner/MainWindow.xaml
@@ -199,6 +199,8 @@
Command="{Binding CanvasResetZoomCommand}" />
+
diff --git a/AnnoDesigner/Models/AppSettings.cs b/AnnoDesigner/Models/AppSettings.cs
index af57f83e..c524f19b 100644
--- a/AnnoDesigner/Models/AppSettings.cs
+++ b/AnnoDesigner/Models/AppSettings.cs
@@ -293,6 +293,12 @@ public bool IncludeRoadsInStatisticCalculation
get => Settings.Default.IncludeRoadsInStatisticCalculation;
set => Settings.Default.IncludeRoadsInStatisticCalculation = value;
}
+
+ public string PresetLayoutLocation
+ {
+ get => Settings.Default.PresetLayoutLocation;
+ set => Settings.Default.PresetLayoutLocation = value;
+ }
}
}
diff --git a/AnnoDesigner/Models/CanvasRenderSetting.cs b/AnnoDesigner/Models/CanvasRenderSetting.cs
new file mode 100644
index 00000000..af28fabd
--- /dev/null
+++ b/AnnoDesigner/Models/CanvasRenderSetting.cs
@@ -0,0 +1,17 @@
+namespace AnnoDesigner.Models
+{
+ public class CanvasRenderSetting
+ {
+ public int? GridSize { get; set; }
+
+ public bool RenderStatistics { get; set; }
+ public bool RenderVersion { get; set; }
+ public bool RenderGrid { get; set; }
+ public bool RenderIcon { get; set; }
+ public bool RenderLabel { get; set; }
+ public bool RenderHarborBlockedArea { get; set; }
+ public bool RenderPanorama { get; set; }
+ public bool RenderTrueInfluenceRange { get; set; }
+ public bool RenderInfluences { get; set; }
+ }
+}
diff --git a/AnnoDesigner/PreferencesPages/GeneralSettingsPage.xaml b/AnnoDesigner/PreferencesPages/GeneralSettingsPage.xaml
index f5d79630..1715ab4d 100644
--- a/AnnoDesigner/PreferencesPages/GeneralSettingsPage.xaml
+++ b/AnnoDesigner/PreferencesPages/GeneralSettingsPage.xaml
@@ -22,7 +22,8 @@
+ Margin="10,5,0,0"
+ Grid.IsSharedSizeScope="True">
@@ -33,7 +34,8 @@
+ Width="Auto"
+ SharedSizeGroup="TitleColumn"/>
@@ -74,12 +76,11 @@
Visibility="{Binding IsGridLineColorPickerVisible, Converter={StaticResource converterBoolToVisibilityCollapsed}}" />
-
+
+ SharedSizeGroup="TitleColumn" />
@@ -120,12 +121,11 @@
Visibility="{Binding IsObjectBorderLineColorPickerVisible, Converter={StaticResource converterBoolToVisibilityCollapsed}}" />
-
+
+ SharedSizeGroup="TitleColumn" />
@@ -190,7 +190,6 @@
-
@@ -204,7 +203,8 @@
+ Width="Auto"
+ SharedSizeGroup="TitleColumn"/>
@@ -241,5 +241,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AnnoDesigner/PresetLayoutViewModel.cs b/AnnoDesigner/PresetLayoutViewModel.cs
new file mode 100644
index 00000000..580da4b4
--- /dev/null
+++ b/AnnoDesigner/PresetLayoutViewModel.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using AnnoDesigner.Core.Layout.Presets;
+using AnnoDesigner.Core.Models;
+
+namespace AnnoDesigner
+{
+ public interface IPresetLayoutHolder
+ {
+ List Presets { get; set; }
+ }
+
+ public class PresetLayoutViewModel : Notify, IPresetLayoutHolder
+ {
+ private List presets;
+ private PresetLayout selectedPreset;
+ private bool loadingLayouts;
+
+ public PresetLayoutLoader PresetLayoutLoader { get; set; }
+
+ public List Presets
+ {
+ get { return presets; }
+ set { UpdateProperty(ref presets, value); }
+ }
+
+ public PresetLayout SelectedPreset
+ {
+ get { return selectedPreset; }
+ set { UpdateProperty(ref selectedPreset, value); }
+ }
+
+ public bool LoadingLayouts
+ {
+ get { return loadingLayouts; }
+ set { UpdateProperty(ref loadingLayouts, value); }
+ }
+
+ public Func BeforeLayoutOpen { get; set; }
+
+ public IAppSettings AppSettings { get; set; }
+
+ public PresetLayoutViewModel()
+ {
+ if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(new DependencyObject()))
+ {
+ Presets = new List()
+ {
+ new PresetLayoutDirectory()
+ {
+ Name = "Directory",
+ Presets = new List()
+ {
+ new PresetLayout()
+ {
+ Info = new LayoutPresetInfo()
+ {
+ Name = new MultilangInfo()
+ {
+ ["eng"] = "English",
+ ["ger"] = "German",
+ ["fra"] = "French",
+ ["esp"] = "Spanish",
+ ["pol"] = "Polish",
+ ["rus"] = "Russian"
+ },
+ Description = "description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description ",
+ Author = "author",
+ AuthorContact = "author@contact.com"
+ },
+ Layout = new Core.Layout.Models.LayoutFile()
+ {
+ LayoutVersion = new Version(2, 1, 3, 12)
+ },
+ Images = new List()
+ }
+ }
+ }
+ };
+ SelectedPreset = (Presets[0] as PresetLayoutDirectory).Presets[0] as PresetLayout;
+ }
+ }
+
+ public async Task LoadLayoutsAsync()
+ {
+ LoadingLayouts = true;
+ Presets = await PresetLayoutLoader.LoadAsync(AppSettings.PresetLayoutLocation);
+ LoadingLayouts = false;
+ }
+ }
+}
diff --git a/AnnoDesigner/PresetLayoutWindow.xaml b/AnnoDesigner/PresetLayoutWindow.xaml
new file mode 100644
index 00000000..1b081920
--- /dev/null
+++ b/AnnoDesigner/PresetLayoutWindow.xaml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AnnoDesigner/PresetLayoutWindow.xaml.cs b/AnnoDesigner/PresetLayoutWindow.xaml.cs
new file mode 100644
index 00000000..a8230ef9
--- /dev/null
+++ b/AnnoDesigner/PresetLayoutWindow.xaml.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+using AnnoDesigner.Core.Layout.Presets;
+using AnnoDesigner.Models;
+
+namespace AnnoDesigner
+{
+ ///
+ /// Interaction logic for PresetLayoutWindow.xaml
+ ///
+ public partial class PresetLayoutWindow : Window
+ {
+ public PresetLayoutViewModel Context
+ {
+ get => DataContext as PresetLayoutViewModel;
+ set => DataContext = value;
+ }
+
+ public PresetLayoutWindow()
+ {
+ InitializeComponent();
+
+ var timer = new DispatcherTimer();
+ timer.Interval = TimeSpan.FromSeconds(10);
+ timer.Tick += Timer_Tick;
+ timer.Start();
+ }
+
+ private void Timer_Tick(object sender, EventArgs e)
+ {
+ static IEnumerable getRecursively(IPresetLayout preset)
+ {
+ if (preset is PresetLayout presetLayout)
+ {
+ yield return presetLayout;
+ }
+ if (preset is PresetLayoutDirectory presetLayoutDirectory)
+ {
+ foreach (var item in presetLayoutDirectory.Presets.SelectMany(getRecursively))
+ {
+ yield return item;
+ }
+ }
+ }
+
+ foreach (var item in Context.Presets.SelectMany(getRecursively))
+ {
+ if (item != Context.SelectedPreset)
+ {
+ item.UnloadImages();
+ }
+ }
+ }
+
+ private async void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs