diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs
index dba5e36f8..186644a2a 100644
--- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs
+++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs
@@ -34,6 +34,36 @@ namespace Artemis.Core.Models.Profile.Conditions
///
public abstract string Icon { get; }
+ ///
+ /// Gets or sets whether this condition operator supports a right side, defaults to true
+ ///
+ public bool SupportsRightSide { get; protected set; } = true;
+
+ public bool SupportsType(Type type)
+ {
+ if (type == null)
+ return true;
+ return CompatibleTypes.Any(t => t.IsCastableFrom(type));
+ }
+
+ ///
+ /// Creates a binary expression comparing two types
+ ///
+ /// The parameter on the left side of the expression
+ /// The parameter on the right side of the expression
+ ///
+ public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide);
+
+ ///
+ /// Returns an expression that checks the given expression for null
+ ///
+ ///
+ ///
+ protected Expression CreateNullCheckExpression(Expression expression)
+ {
+ return Expression.NotEqual(expression, Expression.Constant(null));
+ }
+
internal void Register(PluginInfo pluginInfo, IDataModelService dataModelService)
{
if (_registered)
@@ -63,20 +93,5 @@ namespace Artemis.Core.Models.Profile.Conditions
// Profile editor service will call Unsubscribe
_dataModelService.RemoveConditionOperator(this);
}
-
- public bool SupportsType(Type type)
- {
- if (type == null)
- return true;
- return CompatibleTypes.Any(t => t.IsCastableFrom(type));
- }
-
- ///
- /// Creates a binary expression comparing two types
- ///
- /// The parameter on the left side of the expression
- /// The parameter on the right side of the expression
- ///
- public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide);
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs
index 7da7e87ca..eeb31bf6c 100644
--- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs
+++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs
@@ -304,7 +304,11 @@ namespace Artemis.Core.Models.Profile.Conditions
if (leftSideAccessor.Type.IsValueType && RightStaticValue == null)
return;
- var rightSideConstant = Expression.Constant(RightStaticValue);
+ // If the right side value is null, the constant type cannot be inferred and must be provided manually
+ var rightSideConstant = RightStaticValue != null
+ ? Expression.Constant(RightStaticValue)
+ : Expression.Constant(null, leftSideAccessor.Type);
+
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant);
StaticConditionLambda = Expression.Lambda>(conditionExpression, leftSideParameter);
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs
new file mode 100644
index 000000000..f82d7e367
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringContainsConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+ private readonly MethodInfo _contains;
+
+ public StringContainsConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ _contains = typeof(string).GetMethod("Contains", new[] {typeof(string) });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)};
+
+ public override string Description => "Contains";
+ public override string Icon => "Contain";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)), Expression.Constant(true));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs
new file mode 100644
index 000000000..69bcdc5b3
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringEndsWithConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+ private readonly MethodInfo _endsWith;
+
+ public StringEndsWithConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ _endsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) };
+
+ public override string Description => "Ends with";
+ public override string Icon => "ContainEnd";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _endsWith, Expression.Call(rightSide, _toLower)), Expression.Constant(true));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs
new file mode 100644
index 000000000..56fce09f7
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringEqualsConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+
+ public StringEqualsConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)};
+
+ public override string Description => "Equals";
+ public override string Icon => "Equal";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs
new file mode 100644
index 000000000..11ee0144b
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringNotContainsConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+ private readonly MethodInfo _contains;
+
+ public StringNotContainsConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ _contains = typeof(string).GetMethod("Contains", new[] { typeof(string) });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) };
+
+ public override string Description => "Does not contain";
+ public override string Icon => "FormatStrikethrough";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)), Expression.Constant(false));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs
new file mode 100644
index 000000000..7235dd5bc
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringNotEqualConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+
+ public StringNotEqualConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)};
+
+ public override string Description => "Does not equal";
+ public override string Icon => "NotEqualVariant";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.NotEqual(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs
new file mode 100644
index 000000000..589b31223
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringNullConditionOperator : DisplayConditionOperator
+ {
+ public StringNullConditionOperator()
+ {
+ SupportsRightSide = false;
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)};
+
+ public override string Description => "Is null";
+ public override string Icon => "Null";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(leftSide, Expression.Constant(null, leftSide.Type));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs
new file mode 100644
index 000000000..06259737c
--- /dev/null
+++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Artemis.Core.Models.Profile.Conditions.Operators
+{
+ public class StringStartsWithConditionOperator : DisplayConditionOperator
+ {
+ private readonly MethodInfo _toLower;
+ private readonly MethodInfo _startsWith;
+
+ public StringStartsWithConditionOperator()
+ {
+ _toLower = typeof(string).GetMethod("ToLower", new Type[] { });
+ _startsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
+ }
+
+ public override IReadOnlyCollection CompatibleTypes => new List { typeof(string) };
+
+ public override string Description => "Starts with";
+ public override string Icon => "ContainStart";
+
+ public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
+ {
+ return Expression.Equal(Expression.Call(Expression.Call(leftSide, _toLower), _startsWith, Expression.Call(rightSide, _toLower)), Expression.Constant(true));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs
index 3cb5f73f0..a8b2444c1 100644
--- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs
+++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs
@@ -134,10 +134,14 @@ namespace Artemis.Core.Models.Surface
internal void ApplyToRgbDevice()
{
- RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y);
RgbDevice.Rotation = DeviceEntity.Rotation;
RgbDevice.Scale = DeviceEntity.Scale;
+ // Workaround for device rotation not applying
+ if (DeviceEntity.X == 0 && DeviceEntity.Y == 0)
+ RgbDevice.Location = new Point(1, 1);
+ RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y);
+
CalculateRenderProperties();
OnDeviceUpdated();
}
diff --git a/src/Artemis.Core/Services/DataModelService.cs b/src/Artemis.Core/Services/DataModelService.cs
index ba67bf455..4c29ecadc 100644
--- a/src/Artemis.Core/Services/DataModelService.cs
+++ b/src/Artemis.Core/Services/DataModelService.cs
@@ -151,7 +151,21 @@ namespace Artemis.Core.Services
{
if (type == null)
return new List(_registeredConditionOperators);
- return _registeredConditionOperators.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList();
+
+ var candidates = _registeredConditionOperators.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList();
+
+ // If there are multiple operators with the same description, use the closest match
+ foreach (var displayConditionOperators in candidates.GroupBy(c => c.Description).Where(g => g.Count() > 1).ToList())
+ {
+ var bestCandidate = displayConditionOperators.OrderByDescending(c => c.CompatibleTypes.Contains(type)).FirstOrDefault();
+ foreach (var displayConditionOperator in displayConditionOperators)
+ {
+ if (displayConditionOperator != bestCandidate)
+ candidates.Remove(displayConditionOperator);
+ }
+ }
+
+ return candidates;
}
}
@@ -162,12 +176,24 @@ namespace Artemis.Core.Services
private void RegisterBuiltInConditionOperators()
{
+ // General usage for any type
RegisterConditionOperator(Constants.CorePluginInfo, new EqualsConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator());
+
+ // Numeric operators
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanOrEqualConditionOperator());
+
+ // String operators
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringEqualsConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringNotEqualConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringContainsConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator());
+ RegisterConditionOperator(Constants.CorePluginInfo, new StringNullConditionOperator());
}
private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e)
diff --git a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs
new file mode 100644
index 000000000..e06761e8d
--- /dev/null
+++ b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Artemis.Storage.Entities.Profile;
+using Artemis.Storage.Migrations.Interfaces;
+using LiteDB;
+
+namespace Artemis.Storage.Migrations
+{
+ public class M4ProfileSegmentsMigration : IStorageMigration
+ {
+ public int UserVersion => 4;
+
+ public void Apply(LiteRepository repository)
+ {
+ var profiles = repository.Query().ToList();
+ foreach (var profileEntity in profiles)
+ {
+ foreach (var folder in profileEntity.Folders.Where(f => f.MainSegmentLength == TimeSpan.Zero))
+ {
+ if (folder.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
+ folder.MainSegmentLength = folder.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
+ if (folder.MainSegmentLength == TimeSpan.Zero)
+ folder.MainSegmentLength = TimeSpan.FromSeconds(5);
+ }
+
+ foreach (var layer in profileEntity.Layers.Where(l => l.MainSegmentLength == TimeSpan.Zero))
+ {
+ if (layer.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
+ layer.MainSegmentLength = layer.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
+ if (layer.MainSegmentLength == TimeSpan.Zero)
+ layer.MainSegmentLength = TimeSpan.FromSeconds(5);
+ }
+
+ repository.Update(profileEntity);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Storage/Repositories/PluginRepository.cs b/src/Artemis.Storage/Repositories/PluginRepository.cs
index 7d0f79652..d31f33a31 100644
--- a/src/Artemis.Storage/Repositories/PluginRepository.cs
+++ b/src/Artemis.Storage/Repositories/PluginRepository.cs
@@ -12,7 +12,7 @@ namespace Artemis.Storage.Repositories
internal PluginRepository(LiteRepository repository)
{
_repository = repository;
-
+
_repository.Database.GetCollection().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
}
diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs
index eb06ec9b2..051a601d9 100644
--- a/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs
+++ b/src/Artemis.UI/Screens/Module/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Artemis.Core.Models.Profile.Conditions;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.Abstract;
+using Artemis.UI.Shared.Services.Interfaces;
using Humanizer;
using Stylet;
@@ -11,13 +12,15 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions
{
public class DisplayConditionGroupViewModel : DisplayConditionViewModel
{
+ private readonly IProfileEditorService _profileEditorService;
private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory;
private bool _isRootGroup;
private bool _isInitialized;
- public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent, IDisplayConditionsVmFactory displayConditionsVmFactory) : base(
- displayConditionGroup, parent)
+ public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent,
+ IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) : base(displayConditionGroup, parent)
{
+ _profileEditorService = profileEditorService;
_displayConditionsVmFactory = displayConditionsVmFactory;
Execute.PostToUIThread(async () =>
{
@@ -55,13 +58,17 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions
DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Static));
else if (type == "Dynamic")
DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Dynamic));
+
Update();
+ _profileEditorService.UpdateSelectedProfileElement();
}
public void AddGroup()
{
DisplayConditionGroup.AddChild(new DisplayConditionGroup(DisplayConditionGroup));
+
Update();
+ _profileEditorService.UpdateSelectedProfileElement();
}
public override void Update()
diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs
index 88427dc2e..cdc5c2fe0 100644
--- a/src/Artemis.UI/Screens/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/RootViewModel.cs
@@ -254,6 +254,7 @@ namespace Artemis.UI.Screens
protected override void OnClose()
{
SidebarViewModel.Dispose();
+
// Lets force the GC to run after closing the window so it is obvious to users watching task manager
// that closing the UI will decrease the memory footprint of the application.
diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
index 4eb80341c..c45634af6 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
@@ -197,6 +197,9 @@ namespace Artemis.UI.Screens.Sidebar
public void Dispose()
{
+ var closeTask = CloseCurrentItem();
+ closeTask.Wait();
+
_pluginService.PluginEnabled -= PluginServiceOnPluginEnabled;
_pluginService.PluginDisabled -= PluginServiceOnPluginDisabled;
}
diff --git a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs
index a9e01ffed..cbe76d302 100644
--- a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs
+++ b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs
@@ -38,8 +38,10 @@ namespace Artemis.Plugins.Modules.General
public void UpdateCurrentWindow()
{
var processId = WindowUtilities.GetActiveProcessId();
- if (DataModel.ActiveWindow == null || DataModel.ActiveWindow.Process.Id != processId)
+ if (DataModel.ActiveWindow == null || DataModel.ActiveWindow.Process.Id != processId)
DataModel.ActiveWindow = new WindowDataModel(Process.GetProcessById(processId));
+ if (DataModel.ActiveWindow != null && string.IsNullOrWhiteSpace(DataModel.ActiveWindow.WindowTitle))
+ DataModel.ActiveWindow.WindowTitle = Process.GetProcessById(WindowUtilities.GetActiveProcessId()).MainWindowTitle;
}
#endregion