From fdb4633d3e9158e457d57e820df6e1efb4df39c2 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 7 May 2023 15:22:54 +0200 Subject: Update to support notes + new style --- .../AccentColor.colorset/Contents.json | 6 +- .../AppIcon.appiconset/icon_128x128.png | Bin 5641 -> 9258 bytes .../AppIcon.appiconset/icon_128x128@2x.png | Bin 13167 -> 24714 bytes .../AppIcon.appiconset/icon_16x16.png | Bin 628 -> 629 bytes .../AppIcon.appiconset/icon_16x16@2x.png | Bin 1367 -> 1588 bytes .../AppIcon.appiconset/icon_256x256.png | Bin 12374 -> 24153 bytes .../AppIcon.appiconset/icon_256x256@2x.png | Bin 29179 -> 69122 bytes .../AppIcon.appiconset/icon_32x32.png | Bin 1367 -> 1588 bytes .../AppIcon.appiconset/icon_32x32@2x.png | Bin 2285 -> 3343 bytes .../AppIcon.appiconset/icon_512x512.png | Bin 29179 -> 69122 bytes .../AppIcon.appiconset/icon_512x512@2x.png | Bin 74672 -> 216083 bytes .../Colors/Background.colorset/Contents.json | 38 ++++ Map/Assets.xcassets/Colors/Contents.json | 6 + .../Colors/Foreground.colorset/Contents.json | 38 ++++ Map/Assets.xcassets/Colors/Syntax/Contents.json | 6 + .../Colors/Syntax/Number.colorset/Contents.json | 20 +++ .../Colors/Syntax/Option.colorset/Contents.json | 20 +++ .../Colors/Syntax/Symbol.colorset/Contents.json | 20 +++ .../Colors/Syntax/Vertex.colorset/Contents.json | 38 ++++ Map/Assets.xcassets/Colors/Theme/Contents.json | 6 + .../Theme/Dark Neutral Gray.colorset/Contents.json | 20 +++ .../Colors/Theme/Dark Slate.colorset/Contents.json | 20 +++ .../Colors/Theme/Jasper Red.colorset/Contents.json | 20 +++ .../Light Neutral Gray.colorset/Contents.json | 20 +++ .../Theme/Neutral Gray.colorset/Contents.json | 20 +++ .../Theme/Olympic Blue.colorset/Contents.json | 20 +++ Map/Core Extensions/Color+Theme.swift | 27 +++ Map/Core Extensions/Date+format.swift | 17 ++ Map/Core Extensions/Font+Theme.swift | 8 + Map/Core Extensions/NSColor+Theme.swift | 10 ++ Map/Core Extensions/NSImage+writePNG.swift | 25 +++ Map/Data/AppState.swift | 103 +++++++++++ Map/Data/Models/Map+parse.swift | 29 +++ Map/Data/Persistence.swift | 42 +++++ Map/Data/Stage.swift | 195 +++++++++++++++++++++ Map/Data/Store.swift | 18 ++ Map/Debouncer.swift | 21 --- Map/Extensions/Date+format.swift | 17 -- Map/Extensions/Map+parse.swift | 28 --- Map/Extensions/NSImage+writePNG.swift | 25 --- Map/Logic/Debouncer.swift | 21 +++ Map/Logic/MapParser/MapParser.swift | 157 +++++++++++++++++ .../Strategies/BlockerParserStrategy.swift | 26 +++ .../MapParser/Strategies/EdgeParserStrategy.swift | 32 ++++ .../MapParser/Strategies/NoteParserStrategy.swift | 31 ++++ .../Strategies/OpportunityParserStrategy.swift | 33 ++++ .../MapParser/Strategies/StageParserStrategy.swift | 24 +++ .../Strategies/VertexParserStrategy.swift | 36 ++++ Map/MapApp.swift | 23 +++ Map/MapColor.swift | 65 ------- Map/MapParser/MapParser.swift | 142 --------------- .../Strategies/BlockerParserStrategy.swift | 26 --- Map/MapParser/Strategies/EdgeParserStrategy.swift | 32 ---- .../Strategies/OpportunityParserStrategy.swift | 33 ---- Map/MapParser/Strategies/StageParserStrategy.swift | 24 --- .../Strategies/VertexParserStrategy.swift | 36 ---- Map/MapRenderComponents/MapAxes.swift | 89 ---------- Map/MapRenderComponents/MapBlockers.swift | 49 ------ Map/MapRenderComponents/MapEdges.swift | 92 ---------- Map/MapRenderComponents/MapOpportunities.swift | 84 --------- Map/MapRenderComponents/MapStages.swift | 60 ------- Map/MapRenderComponents/MapVertices.swift | 90 ---------- .../Base Components/EvolutionPicker.swift | 41 +++++ .../Base Components/MapRender/MapAxes.swift | 81 +++++++++ .../Base Components/MapRender/MapBlockers.swift | 43 +++++ .../Base Components/MapRender/MapEdges.swift | 79 +++++++++ .../Base Components/MapRender/MapNotes.swift | 46 +++++ .../MapRender/MapOpportunities.swift | 71 ++++++++ .../Base Components/MapRender/MapStages.swift | 52 ++++++ .../Base Components/MapRender/MapVertices.swift | 92 ++++++++++ .../Base Components/MapTextEditor.swift | 154 ++++++++++++++++ .../MapRender/MapRenderView.swift | 63 +++++++ .../Screens/EmptyMapDetailScreen.swift | 16 ++ Map/Presentation/Screens/MapDetailScreen.swift | 72 ++++++++ Map/Presentation/Windows/MapEditorWindow.swift | 109 ++++++++++++ Map/State/AppState.swift | 103 ----------- Map/State/Persistence.swift | 42 ----- Map/State/Stage.swift | 195 --------------------- Map/State/Store.swift | 18 -- Map/Views/ContentView.swift | 116 ------------ Map/Views/DefaultMapView.swift | 23 --- Map/Views/EvolutionPicker.swift | 48 ----- Map/Views/MapApp.swift | 23 --- Map/Views/MapDetail.swift | 103 ----------- Map/Views/MapRender.swift | 66 ------- Map/Views/MapTextEditor.swift | 148 ---------------- Map/Views/SlowMapRender.swift | 90 ---------- 87 files changed, 2091 insertions(+), 1891 deletions(-) create mode 100644 Map/Assets.xcassets/Colors/Background.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Foreground.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Syntax/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Syntax/Number.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Syntax/Option.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Syntax/Symbol.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Syntax/Vertex.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Dark Neutral Gray.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Dark Slate.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Jasper Red.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Light Neutral Gray.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Neutral Gray.colorset/Contents.json create mode 100644 Map/Assets.xcassets/Colors/Theme/Olympic Blue.colorset/Contents.json create mode 100644 Map/Core Extensions/Color+Theme.swift create mode 100644 Map/Core Extensions/Date+format.swift create mode 100644 Map/Core Extensions/Font+Theme.swift create mode 100644 Map/Core Extensions/NSColor+Theme.swift create mode 100644 Map/Core Extensions/NSImage+writePNG.swift create mode 100644 Map/Data/AppState.swift create mode 100644 Map/Data/Models/Map+parse.swift create mode 100644 Map/Data/Persistence.swift create mode 100644 Map/Data/Stage.swift create mode 100644 Map/Data/Store.swift delete mode 100644 Map/Debouncer.swift delete mode 100644 Map/Extensions/Date+format.swift delete mode 100644 Map/Extensions/Map+parse.swift delete mode 100644 Map/Extensions/NSImage+writePNG.swift create mode 100644 Map/Logic/Debouncer.swift create mode 100644 Map/Logic/MapParser/MapParser.swift create mode 100644 Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift create mode 100644 Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift create mode 100644 Map/Logic/MapParser/Strategies/NoteParserStrategy.swift create mode 100644 Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift create mode 100644 Map/Logic/MapParser/Strategies/StageParserStrategy.swift create mode 100644 Map/Logic/MapParser/Strategies/VertexParserStrategy.swift create mode 100644 Map/MapApp.swift delete mode 100644 Map/MapColor.swift delete mode 100644 Map/MapParser/MapParser.swift delete mode 100644 Map/MapParser/Strategies/BlockerParserStrategy.swift delete mode 100644 Map/MapParser/Strategies/EdgeParserStrategy.swift delete mode 100644 Map/MapParser/Strategies/OpportunityParserStrategy.swift delete mode 100644 Map/MapParser/Strategies/StageParserStrategy.swift delete mode 100644 Map/MapParser/Strategies/VertexParserStrategy.swift delete mode 100644 Map/MapRenderComponents/MapAxes.swift delete mode 100644 Map/MapRenderComponents/MapBlockers.swift delete mode 100644 Map/MapRenderComponents/MapEdges.swift delete mode 100644 Map/MapRenderComponents/MapOpportunities.swift delete mode 100644 Map/MapRenderComponents/MapStages.swift delete mode 100644 Map/MapRenderComponents/MapVertices.swift create mode 100644 Map/Presentation/Base Components/EvolutionPicker.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapAxes.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapBlockers.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapEdges.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapNotes.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapOpportunities.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapStages.swift create mode 100644 Map/Presentation/Base Components/MapRender/MapVertices.swift create mode 100644 Map/Presentation/Base Components/MapTextEditor.swift create mode 100644 Map/Presentation/Complex Components/MapRender/MapRenderView.swift create mode 100644 Map/Presentation/Screens/EmptyMapDetailScreen.swift create mode 100644 Map/Presentation/Screens/MapDetailScreen.swift create mode 100644 Map/Presentation/Windows/MapEditorWindow.swift delete mode 100644 Map/State/AppState.swift delete mode 100644 Map/State/Persistence.swift delete mode 100644 Map/State/Stage.swift delete mode 100644 Map/State/Store.swift delete mode 100644 Map/Views/ContentView.swift delete mode 100644 Map/Views/DefaultMapView.swift delete mode 100644 Map/Views/EvolutionPicker.swift delete mode 100644 Map/Views/MapApp.swift delete mode 100644 Map/Views/MapDetail.swift delete mode 100644 Map/Views/MapRender.swift delete mode 100644 Map/Views/MapTextEditor.swift delete mode 100644 Map/Views/SlowMapRender.swift (limited to 'Map') diff --git a/Map/Assets.xcassets/AccentColor.colorset/Contents.json b/Map/Assets.xcassets/AccentColor.colorset/Contents.json index c981db7..b90aa6c 100644 --- a/Map/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Map/Assets.xcassets/AccentColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.969", - "green" : "0.871", - "red" : "1.000" + "blue" : "0xF0", + "green" : "0xB3", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png index b4618d4..b5ff983 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png index dfdf396..109328a 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png index fb22da2..31acec7 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png index a2ea184..d9a0db3 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png index cbe1cfc..b6a0286 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png index 64bc6b5..d674c53 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png index a2ea184..d9a0db3 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png index 5663b5e..650d9ff 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png index 64bc6b5..d674c53 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png index bfa4365..5b5cdd6 100644 Binary files a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/Map/Assets.xcassets/Colors/Background.colorset/Contents.json b/Map/Assets.xcassets/Colors/Background.colorset/Contents.json new file mode 100644 index 0000000..d55bcdc --- /dev/null +++ b/Map/Assets.xcassets/Colors/Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1F", + "green" : "0x26", + "red" : "0x0F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Contents.json b/Map/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Foreground.colorset/Contents.json b/Map/Assets.xcassets/Colors/Foreground.colorset/Contents.json new file mode 100644 index 0000000..6a9d7a1 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Foreground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1F", + "green" : "0x26", + "red" : "0x0F" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Syntax/Contents.json b/Map/Assets.xcassets/Colors/Syntax/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Syntax/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Syntax/Number.colorset/Contents.json b/Map/Assets.xcassets/Colors/Syntax/Number.colorset/Contents.json new file mode 100644 index 0000000..890992f --- /dev/null +++ b/Map/Assets.xcassets/Colors/Syntax/Number.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.902", + "green" : "0.561", + "red" : "0.310" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Syntax/Option.colorset/Contents.json b/Map/Assets.xcassets/Colors/Syntax/Option.colorset/Contents.json new file mode 100644 index 0000000..bad8227 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Syntax/Option.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x2B", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Syntax/Symbol.colorset/Contents.json b/Map/Assets.xcassets/Colors/Syntax/Symbol.colorset/Contents.json new file mode 100644 index 0000000..8bea0d3 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Syntax/Symbol.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA2", + "green" : "0xA6", + "red" : "0x90" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Syntax/Vertex.colorset/Contents.json b/Map/Assets.xcassets/Colors/Syntax/Vertex.colorset/Contents.json new file mode 100644 index 0000000..2aa2733 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Syntax/Vertex.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.474", + "green" : "0.671", + "red" : "0.180" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCC", + "green" : "0xFF", + "red" : "0x80" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Contents.json b/Map/Assets.xcassets/Colors/Theme/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Dark Neutral Gray.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Dark Neutral Gray.colorset/Contents.json new file mode 100644 index 0000000..8bea0d3 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Dark Neutral Gray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA2", + "green" : "0xA6", + "red" : "0x90" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Dark Slate.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Dark Slate.colorset/Contents.json new file mode 100644 index 0000000..1d36938 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Dark Slate.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1F", + "green" : "0x26", + "red" : "0x0F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Jasper Red.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Jasper Red.colorset/Contents.json new file mode 100644 index 0000000..bad8227 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Jasper Red.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x2B", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Light Neutral Gray.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Light Neutral Gray.colorset/Contents.json new file mode 100644 index 0000000..f6a9b18 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Light Neutral Gray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE3", + "green" : "0xE6", + "red" : "0xDA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Neutral Gray.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Neutral Gray.colorset/Contents.json new file mode 100644 index 0000000..2e346d1 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Neutral Gray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCC", + "green" : "0xD1", + "red" : "0xB5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Assets.xcassets/Colors/Theme/Olympic Blue.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Olympic Blue.colorset/Contents.json new file mode 100644 index 0000000..9e62414 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/Olympic Blue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE6", + "green" : "0x8F", + "red" : "0x4F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Core Extensions/Color+Theme.swift b/Map/Core Extensions/Color+Theme.swift new file mode 100644 index 0000000..ffbd224 --- /dev/null +++ b/Map/Core Extensions/Color+Theme.swift @@ -0,0 +1,27 @@ +import SwiftUI + +extension Color { + struct theme { + static let darkSlate = Color("Dark Slate") + static let jasperRed = Color("Jasper Red") + static let olympicBlue = Color("Olympic Blue") + static let neutralGray = Color("Neutral Gray") + static let lightNeutralGray = Color("Light Neutral Gray") + static let darkNeutralGray = Color("Dark Neutral Gray") + } + + struct map { + static let labelColor = Color.theme.darkSlate + static let axisColor = Color.theme.darkSlate + static let vertexColor = Color.theme.darkSlate + static let blockerColor = Color.theme.jasperRed + static let opportunityColor = Color.theme.olympicBlue + static let stageForeground = Color.theme.lightNeutralGray + static let stageBackground = Color.white + } + + struct ui { + static let foreground = Color("Foreground") + static let background = Color("Background") + } +} diff --git a/Map/Core Extensions/Date+format.swift b/Map/Core Extensions/Date+format.swift new file mode 100644 index 0000000..d29ed0f --- /dev/null +++ b/Map/Core Extensions/Date+format.swift @@ -0,0 +1,17 @@ +// +// Date+format.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import Foundation + +extension Date { + func format() -> String { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter.string(from: self) + } +} diff --git a/Map/Core Extensions/Font+Theme.swift b/Map/Core Extensions/Font+Theme.swift new file mode 100644 index 0000000..30bc6f9 --- /dev/null +++ b/Map/Core Extensions/Font+Theme.swift @@ -0,0 +1,8 @@ +import SwiftUI + +public extension Font { + struct theme { + static let axisLabel = Font.custom("Hiragino Mincho ProN", size: 14) + static let vertexLabel = Font.custom("Hiragino Mincho ProN", size: 12) + } +} diff --git a/Map/Core Extensions/NSColor+Theme.swift b/Map/Core Extensions/NSColor+Theme.swift new file mode 100644 index 0000000..0ef1094 --- /dev/null +++ b/Map/Core Extensions/NSColor+Theme.swift @@ -0,0 +1,10 @@ +import AppKit + +extension NSColor { + struct syntax { + static let vertex = NSColor(named: "Vertex") ?? .textColor + static let number = NSColor(named: "Number") ?? .textColor + static let option = NSColor(named: "Option") ?? .textColor + static let symbol = NSColor(named: "Symbol") ?? .textColor + } +} diff --git a/Map/Core Extensions/NSImage+writePNG.swift b/Map/Core Extensions/NSImage+writePNG.swift new file mode 100644 index 0000000..c24c0cf --- /dev/null +++ b/Map/Core Extensions/NSImage+writePNG.swift @@ -0,0 +1,25 @@ +import Cocoa + +extension NSImage { + public func writePNG(toURL url: URL) { + + guard let data = tiffRepresentation, + let rep = NSBitmapImageRep(data: data), + let imgData = rep.representation( + using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 1.0)]) + else { + + print( + "\(self.self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)" + ) + return + } + + do { + try imgData.write(to: url) + } catch let error { + print( + "\(self.self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)") + } + } +} diff --git a/Map/Data/AppState.swift b/Map/Data/AppState.swift new file mode 100644 index 0000000..d0d5670 --- /dev/null +++ b/Map/Data/AppState.swift @@ -0,0 +1,103 @@ +import Cocoa +import Foundation +import SwiftUI + +struct AppState { + var selectedEvolution: StageType = .general +} + +enum AppAction { + case selectEvolution(evolution: StageType) + case exportMapAsImage(map: Map) + case exportMapAsText(map: Map) + case deleteMap(map: Map) +} + +func appStateReducer(state: inout AppState, action: AppAction) { + + switch action { + + case .selectEvolution(let evolution): + state.selectedEvolution = evolution + + case .exportMapAsImage(let map): + let window = NSWindow( + contentRect: .init( + origin: .zero, + size: .init( + width: NSScreen.main!.frame.width, + height: NSScreen.main!.frame.height)), + styleMask: [.closable], + backing: .buffered, + defer: false) + + window.title = map.title ?? "Untitled Map" + window.isOpaque = true + window.center() + window.isMovableByWindowBackground = true + window.makeKeyAndOrderFront(nil) + + let renderView = MapRenderView( + content: Binding.constant(map.content ?? ""), + evolution: Binding.constant(state.selectedEvolution)) + + let view = NSHostingView(rootView: renderView) + window.contentView = view + + let imageRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds)! + view.cacheDisplay(in: view.bounds, to: imageRepresentation) + let image = NSImage(cgImage: imageRepresentation.cgImage!, size: view.bounds.size) + + let dialog = NSSavePanel() + + dialog.title = "Save Map" + dialog.showsResizeIndicator = false + dialog.canCreateDirectories = true + dialog.showsHiddenFiles = false + dialog.allowedContentTypes = [.png] + dialog.nameFieldStringValue = map.title ?? "Untitled Map" + + if dialog.runModal() == NSApplication.ModalResponse.OK { + let result = dialog.url + + if result != nil { + + image.writePNG(toURL: result!) + print("saved at \(result!)") + } + } else { + print("Cancel") + } + window.orderOut(nil) + + case .exportMapAsText(let map): + let dialog = NSSavePanel() + + dialog.title = "Save Map Text" + dialog.showsResizeIndicator = false + dialog.canCreateDirectories = true + dialog.showsHiddenFiles = false + dialog.allowedContentTypes = [.text] + dialog.nameFieldStringValue = map.title ?? "Untitled Map" + + if let content = map.content { + + if dialog.runModal() == NSApplication.ModalResponse.OK { + let result = dialog.url + + if let result = result { + try? content.write(to: result, atomically: true, encoding: String.Encoding.utf8) + } + } else { + print("Cancel") + } + } + case .deleteMap(let map): + let context = PersistenceController.shared.container.viewContext + context.delete(map) + + try? context.save() + } +} + +typealias AppStore = Store diff --git a/Map/Data/Models/Map+parse.swift b/Map/Data/Models/Map+parse.swift new file mode 100644 index 0000000..5181daf --- /dev/null +++ b/Map/Data/Models/Map+parse.swift @@ -0,0 +1,29 @@ +extension Map { + static func parse(content: String) -> ParsedMap { + + let parsers = [ + AnyMapParserStrategy(NoteParserStrategy()), + AnyMapParserStrategy(VertexParserStrategy()), + AnyMapParserStrategy(EdgeParserStrategy()), + AnyMapParserStrategy(BlockerParserStrategy()), + AnyMapParserStrategy(OpportunityParserStrategy()), + AnyMapParserStrategy(StageParserStrategy()), + ] + let builder = MapBuilder() + + let lines = content.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + for parser in parsers { + if parser.canHandle(line: String(line)) { + let (type, object) = parser.handle( + index: index, line: String(line), vertices: builder.vertices) + builder.addObjectToMap(type: type, object: object) + break + } + } + } + + return builder.build() + } +} diff --git a/Map/Data/Persistence.swift b/Map/Data/Persistence.swift new file mode 100644 index 0000000..1eb09e8 --- /dev/null +++ b/Map/Data/Persistence.swift @@ -0,0 +1,42 @@ +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newMap = Map(context: viewContext) + newMap.uuid = UUID() + newMap.createdAt = Date() + newMap.title = "Map \(newMap.createdAt!.format())" + newMap.content = "" + } + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Map") + + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.automaticallyMergesChangesFromParent = true + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } +} diff --git a/Map/Data/Stage.swift b/Map/Data/Stage.swift new file mode 100644 index 0000000..26b1929 --- /dev/null +++ b/Map/Data/Stage.swift @@ -0,0 +1,195 @@ +struct Stage { + let i: String + let ii: String + let iii: String + let iv: String + + static func stages(_ type: StageType) -> Stage { + switch type { + case .general: + return Stage( + i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)") + case .practice: + return Stage( + i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") + case .data: + return Stage( + i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") + case .knowledge: + return Stage( + i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") + case .ubiquity: + return Stage( + i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing", + iv: "Widespread in the applicable market / ecosystem") + case .certainty: + return Stage( + i: "Poorly Understood / exploring the unknown", + ii: "Rapid Increase In Learning / discovery becomes refining", + iii: "Rapid increase in use / increasing fit for purpose", + iv: "Commonly understood (in terms of use)") + case .publicationTypes: + return Stage( + i: + "Describe the wonder of the thing / the discovery of some marvel / a new land / an unknown frontier", + ii: + "Focused on build / construct / awareness and learning / many models of explanation / no accepted forms / a wild west", + iii: + "Maintenance / operations / installation / comparison between competing forms / feature analysis", + iv: "Focused on use / increasingly an accepted, almost invisible component") + case .market: + return Stage( + i: "Undefined Market", + ii: "Forming Market / an array of competing forms and models of understanding", + iii: "Growing Market / consolidation to a few competing but more accepted forms", + iv: "Mature Market / stabilised to an accepted form") + case .knowledgeManagement: + return Stage( + i: "Uncertain", ii: "Learning on use / focused on testing prediction", + iii: "Learning on operation / using prediction / verification", iv: "Known / accepted") + case .marketPerception: + return Stage( + i: "Chaotic (non-linear) / domain of the \"crazy\"", ii: "Domain of \"experts\"", + iii: "Increasing expectation of use / domain of \"professionals\"", + iv: "Ordered (appearance of being linear) / trivial / formula to be applied") + case .userPerception: + return Stage( + i: "Different / confusing / exciting / surprising / dangerous", + ii: "Leading edge / emerging / unceirtanty over results", + iii: "Increasingly common / disappointed if not used or available / feeling left behind", + iv: "Standard / expected / feeling of shock if not used") + case .perceptionInIndustry: + return Stage( + i: "Future source of competitive advantage / unpredictable / unknown", + ii: "Seen as a scompetitive advantage / a differential / ROI / case examples", + iii: "Advantage through implementation / features / this model is better than that", + iv: "Cost of doing business / accepted / specific defined models") + case .focusOfValue: + return Stage( + i: "High future worth but immediate investment", + ii: "Seeking ways to profit and a ROI / seeking confirmation of value", + iii: + "High profitability per unit / a valuable model / a feeling of understanding / focus on exploitation", + iv: + "High volume / reducing margin / important but invisible / an essential component of something more complex" + ) + case .understanding: + return Stage( + i: "Poorly Understood / unpredictable", + ii: "Increasing understanding / development of measures", + iii: "Increasing education / constant refinement of needs / measures", + iv: "Believed to be well defined / stable / measurable") + case .comparison: + return Stage( + i: "Constantly changing / a differential / unstable", + ii: "Learning from others / testing the water / some evidential support", + iii: "Competing models / feature difference / evidential support", + iv: "Essential / any advantage is operational / accepted norm") + case .failure: + return Stage( + i: "High / tolerated / assumed to be wrong", + ii: "Moderate / unsurprising if wrong but disappointed", + iii: + "Not tolerated / focus on constant improvement / assumed to be in the right direction / resistance to changing the model", + iv: "Surprised by failure / focus on operational efficiency") + case .marketAction: + return Stage( + i: "Gambling / driven by gut", ii: "Exploring a \"found\" value", + iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") + case .efficiency: + return Stage( + i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", + iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") + case .decisionDrivers: + return Stage( + i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", + iv: "Previous Experience") + case .behavior: + return Stage( + i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", + iv: "Known / common usage") + } + } + + static func title(_ type: StageType) -> String { + switch type { + case .general: + return "Activities" + case .practice: + return "Practice" + case .data: + return "Data" + case .knowledge: + return "Knowledge" + case .ubiquity: + return "Ubiquity" + case .certainty: + return "Certainty" + case .publicationTypes: + return "Publication Types" + case .market: + return "Market" + case .knowledgeManagement: + return "Knowledge Management" + case .marketPerception: + return "Market Perception" + case .userPerception: + return "User Perception" + case .perceptionInIndustry: + return "Perception In Industry" + case .focusOfValue: + return "Focus Of Value" + case .understanding: + return "Understanding" + case .comparison: + return "Comparison" + case .failure: + return "Failure" + case .marketAction: + return "Market Action" + case .efficiency: + return "Efficiency" + case .decisionDrivers: + return "Decision Drivers" + case .behavior: + return "Behavior" + } + } +} + +enum StageType: String, CaseIterable, Identifiable { + case general + case practice + case data + case knowledge + + case ubiquity + case certainty + case publicationTypes + + case market + case knowledgeManagement + case marketPerception + case userPerception + case perceptionInIndustry + case focusOfValue + case understanding + case comparison + case failure + case marketAction + case efficiency + case decisionDrivers + + case behavior + + var id: String { self.rawValue } + + static let types: [StageType] = [.general, .practice, .data, .knowledge] + static let characteristics: [StageType] = [.ubiquity, .certainty, .publicationTypes] + static let properties: [StageType] = [ + .market, .knowledgeManagement, .marketPerception, .userPerception, + .perceptionInIndustry, .focusOfValue, .understanding, .comparison, .failure, + .marketAction, .efficiency, .decisionDrivers, + ] + static let custom: [StageType] = [.behavior] +} diff --git a/Map/Data/Store.swift b/Map/Data/Store.swift new file mode 100644 index 0000000..7860f33 --- /dev/null +++ b/Map/Data/Store.swift @@ -0,0 +1,18 @@ +import Foundation + +final class Store: ObservableObject { + @Published private(set) var state: State + + private let reducer: Reducer + + init(initialState: State, reducer: @escaping Reducer) { + self.state = initialState + self.reducer = reducer + } + + func send(_ action: Action) { + reducer(&state, action) + } +} + +typealias Reducer = (inout State, Action) -> Void diff --git a/Map/Debouncer.swift b/Map/Debouncer.swift deleted file mode 100644 index cd7960a..0000000 --- a/Map/Debouncer.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -class Debouncer { - - // MARK: - Properties - private let queue = DispatchQueue.global(qos: .utility) - private var workItem = DispatchWorkItem(block: {}) - private var interval: TimeInterval - - // MARK: - Initializer - init(seconds: TimeInterval) { - self.interval = seconds - } - - // MARK: - Debouncing function - func debounce(action: @escaping (() -> Void)) { - workItem.cancel() - workItem = DispatchWorkItem(block: { action() }) - queue.asyncAfter(deadline: .now() + interval, execute: workItem) - } -} diff --git a/Map/Extensions/Date+format.swift b/Map/Extensions/Date+format.swift deleted file mode 100644 index d29ed0f..0000000 --- a/Map/Extensions/Date+format.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Date+format.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import Foundation - -extension Date { - func format() -> String { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter.string(from: self) - } -} diff --git a/Map/Extensions/Map+parse.swift b/Map/Extensions/Map+parse.swift deleted file mode 100644 index 904f492..0000000 --- a/Map/Extensions/Map+parse.swift +++ /dev/null @@ -1,28 +0,0 @@ -extension Map { - static func parse(content: String) -> ParsedMap { - - let parsers = [ - AnyMapParserStrategy(VertexParserStrategy()), - AnyMapParserStrategy(EdgeParserStrategy()), - AnyMapParserStrategy(BlockerParserStrategy()), - AnyMapParserStrategy(OpportunityParserStrategy()), - AnyMapParserStrategy(StageParserStrategy()), - ] - let builder = MapBuilder() - - let lines = content.split(whereSeparator: \.isNewline) - - for (index, line) in lines.enumerated() { - for parser in parsers { - if parser.canHandle(line: String(line)) { - let (type, object) = parser.handle( - index: index, line: String(line), vertices: builder.vertices) - builder.addObjectToMap(type: type, object: object) - break - } - } - } - - return builder.build() - } -} diff --git a/Map/Extensions/NSImage+writePNG.swift b/Map/Extensions/NSImage+writePNG.swift deleted file mode 100644 index c24c0cf..0000000 --- a/Map/Extensions/NSImage+writePNG.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Cocoa - -extension NSImage { - public func writePNG(toURL url: URL) { - - guard let data = tiffRepresentation, - let rep = NSBitmapImageRep(data: data), - let imgData = rep.representation( - using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 1.0)]) - else { - - print( - "\(self.self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)" - ) - return - } - - do { - try imgData.write(to: url) - } catch let error { - print( - "\(self.self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)") - } - } -} diff --git a/Map/Logic/Debouncer.swift b/Map/Logic/Debouncer.swift new file mode 100644 index 0000000..cd7960a --- /dev/null +++ b/Map/Logic/Debouncer.swift @@ -0,0 +1,21 @@ +import Foundation + +class Debouncer { + + // MARK: - Properties + private let queue = DispatchQueue.global(qos: .utility) + private var workItem = DispatchWorkItem(block: {}) + private var interval: TimeInterval + + // MARK: - Initializer + init(seconds: TimeInterval) { + self.interval = seconds + } + + // MARK: - Debouncing function + func debounce(action: @escaping (() -> Void)) { + workItem.cancel() + workItem = DispatchWorkItem(block: { action() }) + queue.asyncAfter(deadline: .now() + interval, execute: workItem) + } +} diff --git a/Map/Logic/MapParser/MapParser.swift b/Map/Logic/MapParser/MapParser.swift new file mode 100644 index 0000000..5f78d5d --- /dev/null +++ b/Map/Logic/MapParser/MapParser.swift @@ -0,0 +1,157 @@ +import CoreGraphics +import Foundation + +// MARK: - Types + +struct MapParsingPatterns { + static let vertex = try! NSRegularExpression( + pattern: + "([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?", + options: .caseInsensitive) + static let edge = try! NSRegularExpression( + pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive) + static let blocker = try! NSRegularExpression( + pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive) + static let opportunity = try! NSRegularExpression( + pattern: "\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)", + options: .caseInsensitive) + static let note = try! NSRegularExpression( + pattern: "\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)", + options: .caseInsensitive) + static let stage = try! NSRegularExpression( + pattern: "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: .caseInsensitive) +} + +struct ParsedMap { + let vertices: [Vertex] + let edges: [MapEdge] + let blockers: [Blocker] + let opportunities: [Opportunity] + let notes: [Note] + let stages: [CGFloat] + + static let empty: ParsedMap = ParsedMap( + vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions) +} + +struct Vertex { + let id: Int + let label: String + let position: CGPoint + var shape: VertexShape = .circle +} + +struct Note { + let id: Int + let position: CGPoint + let text: String +} + +enum VertexShape: String { + case circle + case square + case triangle + case x +} + +struct MapEdge { + let id: Int + let origin: CGPoint + let destination: CGPoint + let arrowhead: Bool +} + +struct Blocker { + let id: Int + let position: CGPoint +} + +struct Opportunity { + let id: Int + let origin: CGPoint + let destination: CGPoint +} + +struct StageDimensions { + let index: Int + let dimensions: CGFloat +} + +private let defaultDimensions: [CGFloat] = [ + 25.0, + 50.0, + 75.0, +] + +// MARK: - MapParserStrategy protocol + +protocol MapParserStrategy { + func canHandle(line: String) -> Bool + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) +} + +struct AnyMapParserStrategy: MapParserStrategy { + + private let base: MapParserStrategy + + init(_ base: T) { + self.base = base + } + + func canHandle(line: String) -> Bool { + return base.canHandle(line: line) + } + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + return base.handle(index: index, line: line, vertices: vertices) + } +} + +// MARK: - Map Builder + +class MapBuilder { + var vertices: [String: Vertex] = [:] + private var edges: [MapEdge] = [] + private var blockers: [Blocker] = [] + private var opportunities: [Opportunity] = [] + private var notes: [Note] = [] + private var stages: [CGFloat] = defaultDimensions + + func addObjectToMap(type: Any.Type, object: Any) { + if type == Vertex.self { + let vertex = object as! Vertex + vertices[vertex.label] = vertex + } + + if type == MapEdge.self { + let edge = object as! MapEdge + edges.append(edge) + } + + if type == Blocker.self { + let blocker = object as! Blocker + blockers.append(blocker) + } + + if type == Opportunity.self { + let opportunity = object as! Opportunity + opportunities.append(opportunity) + } + + if type == Note.self { + let note = object as! Note + notes.append(note) + } + + if type == StageDimensions.self { + let stageDimensions = object as! StageDimensions + stages[stageDimensions.index] = stageDimensions.dimensions + } + } + + func build() -> ParsedMap { + let mappedVertices = vertices.map { label, vertex in return vertex } + return ParsedMap( + vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, notes: notes, + stages: stages) + } +} diff --git a/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift b/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift new file mode 100644 index 0000000..d9b6f22 --- /dev/null +++ b/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift @@ -0,0 +1,26 @@ +import Foundation + +struct BlockerParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.blocker + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges == 3 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let vertexA = String(line[Range(match.range(at: 2), in: line)!]) + + if let vertex = vertices[vertexA] { + let blocker = Blocker(id: index, position: vertex.position) + return (Blocker.self, blocker) + } + + return (NSObject.self, NSObject()) // No matching object + } +} diff --git a/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift b/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift new file mode 100644 index 0000000..0df1ff4 --- /dev/null +++ b/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift @@ -0,0 +1,32 @@ +import Foundation + +struct EdgeParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.edge + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges == 4 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let arrowhead = String(line[Range(match.range(at: 2), in: line)!]) == ">" + let vertexA = String(line[Range(match.range(at: 1), in: line)!]) + let vertexB = String(line[Range(match.range(at: 3), in: line)!]) + + if let origin = vertices[vertexA] { + if let destination = vertices[vertexB] { + let edge = MapEdge( + id: index, origin: origin.position, destination: destination.position, + arrowhead: arrowhead) + return (MapEdge.self, edge) + } + } + + return (NSObject.self, NSObject()) // No matching object + } +} diff --git a/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift b/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift new file mode 100644 index 0000000..4dae4c7 --- /dev/null +++ b/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift @@ -0,0 +1,31 @@ +import Foundation + +struct NoteParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.note + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges == 5 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let text = String(line[Range(match.range(at: 4), in: line)!]) + let xString = String(line[Range(match.range(at: 2), in: line)!]) + let yString = String(line[Range(match.range(at: 3), in: line)!]) + let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0) + let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0) + + let note = Note( + id: index, + position: CGPoint(x: x, y: y), + text: text + ) + + return (Note.self, note) + } +} diff --git a/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift new file mode 100644 index 0000000..104fd30 --- /dev/null +++ b/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift @@ -0,0 +1,33 @@ +import Foundation + +struct OpportunityParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.opportunity + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges == 5 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let multiplier = CGFloat( + String(line[Range(match.range(at: 3), in: line)!]) == "-" ? -1.0 : 1.0) + let vertex = String(line[Range(match.range(at: 2), in: line)!]) + let opportunityString = String(line[Range(match.range(at: 4), in: line)!]) + let opportunity = CGFloat( + truncating: NumberFormatter().number(from: opportunityString) ?? 0.0) + + if let origin = vertices[vertex] { + let destination = CGPoint( + x: origin.position.x + opportunity * multiplier, y: origin.position.y) + let opportunity = Opportunity(id: index, origin: origin.position, destination: destination) + return (Opportunity.self, opportunity) + } + + return (NSObject.self, NSObject()) // No matching object + } +} diff --git a/Map/Logic/MapParser/Strategies/StageParserStrategy.swift b/Map/Logic/MapParser/Strategies/StageParserStrategy.swift new file mode 100644 index 0000000..42535fc --- /dev/null +++ b/Map/Logic/MapParser/Strategies/StageParserStrategy.swift @@ -0,0 +1,24 @@ +import Foundation + +struct StageParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.stage + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges == 3 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let stage = String(line[Range(match.range(at: 1), in: line)!]) + let dimensionsString = String(line[Range(match.range(at: 2), in: line)!]) + let dimensions = CGFloat(truncating: NumberFormatter().number(from: dimensionsString) ?? 0.0) + + let stageDimensions = StageDimensions(index: stage.count - 1, dimensions: dimensions) + return (StageDimensions.self, stageDimensions) + } +} diff --git a/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift b/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift new file mode 100644 index 0000000..10c94a2 --- /dev/null +++ b/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift @@ -0,0 +1,36 @@ +import Foundation + +struct VertexParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.vertex + + func canHandle(line: String) -> Bool { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + return matches.count > 0 && matches[0].numberOfRanges >= 4 + } + + func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + let match = matches[0] + let key = String(line[Range(match.range(at: 1), in: line)!]) + let xString = String(line[Range(match.range(at: 2), in: line)!]) + let yString = String(line[Range(match.range(at: 3), in: line)!]) + let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0) + let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0) + + var vertex = Vertex( + id: index, + label: key, + position: CGPoint(x: x, y: y) + ) + + if let range = Range(match.range(at: 4), in: line) { + let shapeString = String(line[range]) + vertex.shape = VertexShape(rawValue: shapeString.lowercased()) ?? .circle + } + + return (Vertex.self, vertex) + } +} diff --git a/Map/MapApp.swift b/Map/MapApp.swift new file mode 100644 index 0000000..14e5605 --- /dev/null +++ b/Map/MapApp.swift @@ -0,0 +1,23 @@ +// +// MapApp.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import SwiftUI + +@main +struct MapApp: App { + let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + MapEditorWindow() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(AppStore(initialState: AppState(), reducer: appStateReducer)) + }.windowStyle(HiddenTitleBarWindowStyle()).commands { + SidebarCommands() + } + } +} diff --git a/Map/MapColor.swift b/Map/MapColor.swift deleted file mode 100644 index 98dd804..0000000 --- a/Map/MapColor.swift +++ /dev/null @@ -1,65 +0,0 @@ -import SwiftUI - -struct MapColor { - let foreground: Color - let background: Color - let secondary: Color - let blocker: Color - let opportunity: Color - let stages: StageColor - let syntax: SyntaxColor - - static func colorForScheme(_ colorScheme: ColorScheme) -> MapColor { - if colorScheme == .dark { - return MapColor( - foreground: Color.white, - background: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), - secondary: Color(.sRGB, red: 0.81, green: 0.78, blue: 0.79), - blocker: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), - opportunity: Color(.sRGB, red: 1.0, green: 0.37, blue: 0.34), - stages: StageColor( - i: Color(.sRGB, red: 0.37, green: 0.16, blue: 0.25), - ii: Color(.sRGB, red: 0.30, green: 0.29, blue: 0.26), - iii: Color(.sRGB, red: 0.15, green: 0.29, blue: 0.23), - iv: Color(.sRGB, red: 0.14, green: 0.22, blue: 0.31)), - syntax: SyntaxColor( - vertex: NSColor(srgbRed: 0.41, green: 0.84, blue: 0.96, alpha: 1.0), - number: NSColor(srgbRed: 0.85, green: 0.78, blue: 0.49, alpha: 1.0), - option: NSColor(srgbRed: 1.0, green: 0.48, blue: 0.7, alpha: 1.0), // #FE7AB3 - symbol: NSColor(srgbRed: 0.85, green: 0.73, blue: 1.0, alpha: 1.0) // #DABBFF - )) - } else { - return MapColor( - foreground: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), - background: Color.white, - secondary: Color.gray, - blocker: Color(.sRGB, red: 0.60, green: 0.52, blue: 0.51), - opportunity: Color(.sRGB, red: 1.0, green: 0.37, blue: 0.34), - stages: StageColor( - i: Color(.sRGB, red: 1.00, green: 0.93, blue: 0.97), - ii: Color(.sRGB, red: 1.00, green: 0.98, blue: 0.92), - iii: Color(.sRGB, red: 0.93, green: 1.00, blue: 0.97), - iv: Color(.sRGB, red: 0.93, green: 0.96, blue: 1.00)), - syntax: SyntaxColor( - vertex: NSColor(srgbRed: 0.11, green: 0.42, blue: 0.57, alpha: 1.0), - number: NSColor(srgbRed: 0.27, green: 0.31, blue: 0.87, alpha: 1.0), - option: NSColor(srgbRed: 0.68, green: 0.24, blue: 0.64, alpha: 1.0), - symbol: NSColor(srgbRed: 0.29, green: 0.13, blue: 0.69, alpha: 1.0) - )) - } - } -} - -struct StageColor { - let i: Color - let ii: Color - let iii: Color - let iv: Color -} - -struct SyntaxColor { - let vertex: NSColor - let number: NSColor - let option: NSColor - let symbol: NSColor -} diff --git a/Map/MapParser/MapParser.swift b/Map/MapParser/MapParser.swift deleted file mode 100644 index cce6251..0000000 --- a/Map/MapParser/MapParser.swift +++ /dev/null @@ -1,142 +0,0 @@ -import CoreGraphics -import Foundation - -// MARK: - Types - -struct MapParsingPatterns { - static let vertex = try! NSRegularExpression( - pattern: - "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?", - options: .caseInsensitive) - static let edge = try! NSRegularExpression( - pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive) - static let blocker = try! NSRegularExpression( - pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive) - static let opportunity = try! NSRegularExpression( - pattern: "\\[(Opportunity)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)", - options: .caseInsensitive) - static let stage = try! NSRegularExpression( - pattern: "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: .caseInsensitive) -} - -struct ParsedMap { - let vertices: [Vertex] - let edges: [MapEdge] - let blockers: [Blocker] - let opportunities: [Opportunity] - let stages: [CGFloat] - - static let empty: ParsedMap = ParsedMap( - vertices: [], edges: [], blockers: [], opportunities: [], stages: defaultDimensions) -} - -struct Vertex { - let id: Int - let label: String - let position: CGPoint - var shape: VertexShape = .circle -} - -enum VertexShape: String { - case circle - case square - case triangle - case x -} - -struct MapEdge { - let id: Int - let origin: CGPoint - let destination: CGPoint - let arrowhead: Bool -} - -struct Blocker { - let id: Int - let position: CGPoint -} - -struct Opportunity { - let id: Int - let origin: CGPoint - let destination: CGPoint -} - -struct StageDimensions { - let index: Int - let dimensions: CGFloat -} - -private let defaultDimensions: [CGFloat] = [ - 25.0, - 50.0, - 75.0, -] - -// MARK: - MapParserStrategy protocol - -protocol MapParserStrategy { - func canHandle(line: String) -> Bool - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) -} - -struct AnyMapParserStrategy: MapParserStrategy { - - private let base: MapParserStrategy - - init(_ base: T) { - self.base = base - } - - func canHandle(line: String) -> Bool { - return base.canHandle(line: line) - } - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - return base.handle(index: index, line: line, vertices: vertices) - } -} - -// MARK: - Map Builder - -class MapBuilder { - var vertices: [String: Vertex] = [:] - private var edges: [MapEdge] = [] - private var blockers: [Blocker] = [] - private var opportunities: [Opportunity] = [] - private var stages: [CGFloat] = defaultDimensions - - func addObjectToMap(type: Any.Type, object: Any) { - if type == Vertex.self { - let vertex = object as! Vertex - vertices[vertex.label] = vertex - } - - if type == MapEdge.self { - let edge = object as! MapEdge - edges.append(edge) - } - - if type == Blocker.self { - let blocker = object as! Blocker - blockers.append(blocker) - } - - if type == Opportunity.self { - let opportunity = object as! Opportunity - opportunities.append(opportunity) - } - - if type == StageDimensions.self { - let stageDimensions = object as! StageDimensions - stages[stageDimensions.index] = stageDimensions.dimensions - - } - } - - func build() -> ParsedMap { - let mappedVertices = vertices.map { label, vertex in return vertex } - return ParsedMap( - vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, - stages: stages) - } -} diff --git a/Map/MapParser/Strategies/BlockerParserStrategy.swift b/Map/MapParser/Strategies/BlockerParserStrategy.swift deleted file mode 100644 index d9b6f22..0000000 --- a/Map/MapParser/Strategies/BlockerParserStrategy.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -struct BlockerParserStrategy: MapParserStrategy { - private let regex = MapParsingPatterns.blocker - - func canHandle(line: String) -> Bool { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - return matches.count > 0 && matches[0].numberOfRanges == 3 - } - - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - let match = matches[0] - let vertexA = String(line[Range(match.range(at: 2), in: line)!]) - - if let vertex = vertices[vertexA] { - let blocker = Blocker(id: index, position: vertex.position) - return (Blocker.self, blocker) - } - - return (NSObject.self, NSObject()) // No matching object - } -} diff --git a/Map/MapParser/Strategies/EdgeParserStrategy.swift b/Map/MapParser/Strategies/EdgeParserStrategy.swift deleted file mode 100644 index 0df1ff4..0000000 --- a/Map/MapParser/Strategies/EdgeParserStrategy.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -struct EdgeParserStrategy: MapParserStrategy { - private let regex = MapParsingPatterns.edge - - func canHandle(line: String) -> Bool { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - return matches.count > 0 && matches[0].numberOfRanges == 4 - } - - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - let match = matches[0] - let arrowhead = String(line[Range(match.range(at: 2), in: line)!]) == ">" - let vertexA = String(line[Range(match.range(at: 1), in: line)!]) - let vertexB = String(line[Range(match.range(at: 3), in: line)!]) - - if let origin = vertices[vertexA] { - if let destination = vertices[vertexB] { - let edge = MapEdge( - id: index, origin: origin.position, destination: destination.position, - arrowhead: arrowhead) - return (MapEdge.self, edge) - } - } - - return (NSObject.self, NSObject()) // No matching object - } -} diff --git a/Map/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/MapParser/Strategies/OpportunityParserStrategy.swift deleted file mode 100644 index 104fd30..0000000 --- a/Map/MapParser/Strategies/OpportunityParserStrategy.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -struct OpportunityParserStrategy: MapParserStrategy { - private let regex = MapParsingPatterns.opportunity - - func canHandle(line: String) -> Bool { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - return matches.count > 0 && matches[0].numberOfRanges == 5 - } - - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - let match = matches[0] - let multiplier = CGFloat( - String(line[Range(match.range(at: 3), in: line)!]) == "-" ? -1.0 : 1.0) - let vertex = String(line[Range(match.range(at: 2), in: line)!]) - let opportunityString = String(line[Range(match.range(at: 4), in: line)!]) - let opportunity = CGFloat( - truncating: NumberFormatter().number(from: opportunityString) ?? 0.0) - - if let origin = vertices[vertex] { - let destination = CGPoint( - x: origin.position.x + opportunity * multiplier, y: origin.position.y) - let opportunity = Opportunity(id: index, origin: origin.position, destination: destination) - return (Opportunity.self, opportunity) - } - - return (NSObject.self, NSObject()) // No matching object - } -} diff --git a/Map/MapParser/Strategies/StageParserStrategy.swift b/Map/MapParser/Strategies/StageParserStrategy.swift deleted file mode 100644 index 42535fc..0000000 --- a/Map/MapParser/Strategies/StageParserStrategy.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -struct StageParserStrategy: MapParserStrategy { - private let regex = MapParsingPatterns.stage - - func canHandle(line: String) -> Bool { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - return matches.count > 0 && matches[0].numberOfRanges == 3 - } - - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - let match = matches[0] - let stage = String(line[Range(match.range(at: 1), in: line)!]) - let dimensionsString = String(line[Range(match.range(at: 2), in: line)!]) - let dimensions = CGFloat(truncating: NumberFormatter().number(from: dimensionsString) ?? 0.0) - - let stageDimensions = StageDimensions(index: stage.count - 1, dimensions: dimensions) - return (StageDimensions.self, stageDimensions) - } -} diff --git a/Map/MapParser/Strategies/VertexParserStrategy.swift b/Map/MapParser/Strategies/VertexParserStrategy.swift deleted file mode 100644 index 10c94a2..0000000 --- a/Map/MapParser/Strategies/VertexParserStrategy.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -struct VertexParserStrategy: MapParserStrategy { - private let regex = MapParsingPatterns.vertex - - func canHandle(line: String) -> Bool { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - return matches.count > 0 && matches[0].numberOfRanges >= 4 - } - - func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - let match = matches[0] - let key = String(line[Range(match.range(at: 1), in: line)!]) - let xString = String(line[Range(match.range(at: 2), in: line)!]) - let yString = String(line[Range(match.range(at: 3), in: line)!]) - let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0) - let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0) - - var vertex = Vertex( - id: index, - label: key, - position: CGPoint(x: x, y: y) - ) - - if let range = Range(match.range(at: 4), in: line) { - let shapeString = String(line[range]) - vertex.shape = VertexShape(rawValue: shapeString.lowercased()) ?? .circle - } - - return (Vertex.self, vertex) - } -} diff --git a/Map/MapRenderComponents/MapAxes.swift b/Map/MapRenderComponents/MapAxes.swift deleted file mode 100644 index a6e2f87..0000000 --- a/Map/MapRenderComponents/MapAxes.swift +++ /dev/null @@ -1,89 +0,0 @@ -import SwiftUI - -struct MapAxes: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let lineWidth: CGFloat - let evolution: Stage - let stages: [CGFloat] - let stageHeight = CGFloat(100.0) - let padding = CGFloat(5.0) - - var color: Color { - MapColor.colorForScheme(colorScheme).foreground - } - - var body: some View { - ZStack(alignment: .topLeading) { - - // Axis Lines - Path { path in - path.move(to: CGPoint(x: 0, y: 0)) - path.addLine(to: CGPoint(x: 0, y: mapSize.height)) - path.addLine(to: CGPoint(x: mapSize.width, y: mapSize.height)) - path.move(to: CGPoint(x: mapSize.width, y: mapSize.height)) - path.closeSubpath() - }.stroke(color, lineWidth: lineWidth * 2) - - // Y Labels - Text("Visible").font(.title3).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) - .offset(CGSize(width: -35.0, height: 0.0)) - Text("Value Chain").font(.title).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) - .offset(CGSize(width: -72.0, height: mapSize.height / 2 - 20)) - Text("Invisible").font(.title3).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) - .offset(CGSize(width: -40.0, height: mapSize.height - 20)) - - // X Labels - - Text("Uncharted") - .font(.title3) - .foregroundColor(color) - .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading) - .offset(CGSize(width: 0.0, height: -stageHeight / 4.0)) - Text("Industrialised") - .font(.title3) - .foregroundColor(color) - .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading) - .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 4.0)) - - Text(evolution.i) - .font(.title3) - .foregroundColor(color) - .frame(width: w(stages[0]), height: stageHeight, alignment: .topLeading) - .offset(CGSize(width: 0.0, height: mapSize.height + padding)) - - Text(evolution.ii) - .font(.title3) - .foregroundColor(color) - .frame(width: w(stages[1]) - w(stages[0]), height: stageHeight, alignment: .topLeading) - .offset(CGSize(width: w(stages[0]), height: mapSize.height + padding)) - - Text(evolution.iii) - .font(.title3) - .foregroundColor(color) - .frame(width: w(stages[2]) - w(stages[1]), height: stageHeight, alignment: .topLeading) - .offset(CGSize(width: w(stages[1]), height: mapSize.height + padding)) - - Text(evolution.iv) - .font(.title3) - .foregroundColor(color) - .frame(width: mapSize.width - w(stages[2]), height: stageHeight, alignment: .topLeading) - .offset(CGSize(width: w(stages[2]), height: mapSize.height + padding)) - } - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } -} - -struct MapAxes_Previews: PreviewProvider { - static var previews: some View { - MapAxes( - mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), - evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0] - ).padding(50.0) - } -} diff --git a/Map/MapRenderComponents/MapBlockers.swift b/Map/MapRenderComponents/MapBlockers.swift deleted file mode 100644 index b668b30..0000000 --- a/Map/MapRenderComponents/MapBlockers.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -struct MapBlockers: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let vertexSize: CGSize - let blockers: [Blocker] - - var color: Color { - MapColor.colorForScheme(colorScheme).blocker - } - - let cornerSize = CGSize(width: 2.0, height: 2.0) - - var body: some View { - ForEach(blockers, id: \.id) { vertex in - Path { path in - path.addRoundedRect( - in: CGRect( - origin: CGPoint( - x: w(vertex.position.x) + 3 * vertexSize.width, - y: h(vertex.position.y) - vertexSize.height * 2 / 3), - size: CGSize(width: vertexSize.width / 2, height: vertexSize.height * 2) - ), cornerSize: cornerSize) - }.fill(color) - } - } - - func h(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } -} - -struct MapBlockers_Previews: PreviewProvider { - static var previews: some View { - MapBlockers( - mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), - blockers: [ - Blocker(id: 0, position: CGPoint(x: 50.0, y: 50.0)), - Blocker(id: 1, position: CGPoint(x: 10.0, y: 20.0)), - ]) - } -} diff --git a/Map/MapRenderComponents/MapEdges.swift b/Map/MapRenderComponents/MapEdges.swift deleted file mode 100644 index d40d2aa..0000000 --- a/Map/MapRenderComponents/MapEdges.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// MapEdges.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/2/21. -// - -import SwiftUI - -struct MapEdges: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let lineWidth: CGFloat - let vertexSize: CGSize - let edges: [MapEdge] - - let arrowheadSize = CGFloat(10.0) - - var color: Color { - MapColor.colorForScheme(colorScheme).foreground - } - - var body: some View { - ForEach(edges, id: \.id) { edge in - Path { path in - - // First we transform edges from percentage to map coordinates - let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) - let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) - - let slope = (destination.y - origin.y) / (destination.x - origin.x) - let angle = atan(slope) - let multiplier = CGFloat(slope < 0 ? -1.0 : 1.0) - let upperAngle = angle - CGFloat.pi / 4.0 - let lowerAngle = angle + CGFloat.pi / 4.0 - - let offsetOrigin = CGPoint( - x: origin.x + multiplier * (vertexSize.width / 2.0) * cos(angle), - y: origin.y + multiplier * (vertexSize.height / 2.0) * sin(angle)) - let offsetDestination = CGPoint( - x: destination.x - multiplier * (vertexSize.width / 2.0) * cos(angle), - y: destination.y - multiplier * (vertexSize.height / 2.0) * sin(angle)) - - path.move(to: offsetOrigin) - path.addLine(to: offsetDestination) - - if edge.arrowhead { - path.move(to: offsetDestination) - path.addLine( - to: CGPoint( - x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), - y: - offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) - - path.move(to: offsetDestination) - path.addLine( - to: CGPoint( - x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), - y: - offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) - } - path.move(to: offsetDestination) - path.closeSubpath() - }.applying( - CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) - ).stroke(color, lineWidth: lineWidth) - } - } - - func h(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } -} - -struct MapEdges_Previews: PreviewProvider { - static var previews: some View { - MapEdges( - mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, - vertexSize: CGSize(width: 25.0, height: 25.0), - edges: [ - MapEdge( - id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2), - arrowhead: true) - ]) - } -} diff --git a/Map/MapRenderComponents/MapOpportunities.swift b/Map/MapRenderComponents/MapOpportunities.swift deleted file mode 100644 index 68d3fb5..0000000 --- a/Map/MapRenderComponents/MapOpportunities.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// MapEdges.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/2/21. -// - -import SwiftUI - -struct MapOpportunities: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let lineWidth: CGFloat - let vertexSize: CGSize - let opportunities: [Opportunity] - - let arrowheadSize = CGFloat(10.0) - - var color: Color { - MapColor.colorForScheme(colorScheme).opportunity - } - - var body: some View { - ForEach(opportunities, id: \.id) { edge in - Path { path in - - // First we transform edges from percentage to map coordinates - let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) - let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) - - let multiplier = CGFloat(edge.destination.x > edge.origin.x ? 1.0 : -1.0) - let upperAngle = -CGFloat.pi / 4.0 - let lowerAngle = CGFloat.pi / 4.0 - - let offsetOrigin = CGPoint(x: origin.x + multiplier * (vertexSize.width / 2.0), y: origin.y) - let offsetDestination = CGPoint( - x: destination.x - multiplier * (vertexSize.width / 2.0), y: destination.y) - - path.move(to: offsetOrigin) - path.addLine(to: offsetDestination) - - path.move(to: offsetDestination) - path.addLine( - to: CGPoint( - x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), - y: - offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) - - path.move(to: offsetDestination) - path.addLine( - to: CGPoint( - x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), - y: - offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) - - path.move(to: offsetDestination) - path.closeSubpath() - }.applying( - CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) - ).strokedPath(StrokeStyle(lineWidth: lineWidth * 2, dash: [10.0])).stroke(color) - } - } - - func h(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } -} - -struct MapOpportunities_Previews: PreviewProvider { - static var previews: some View { - MapOpportunities( - mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, - vertexSize: CGSize(width: 25.0, height: 25.0), - opportunities: [ - Opportunity(id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2)) - ]) - } -} diff --git a/Map/MapRenderComponents/MapStages.swift b/Map/MapRenderComponents/MapStages.swift deleted file mode 100644 index 052a315..0000000 --- a/Map/MapRenderComponents/MapStages.swift +++ /dev/null @@ -1,60 +0,0 @@ -import SwiftUI - -struct MapStages: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let lineWidth: CGFloat - let stages: [CGFloat] - let opacity = 0.1 - - var color: MapColor { - MapColor.colorForScheme(colorScheme) - } - - var body: some View { - - ZStack(alignment: .topLeading) { - Path { path in - path.addRect(CGRect(x: 0, y: 0, width: w(stages[0]), height: mapSize.height)) - }.fill(color.stages.i) - Path { path in - path.addRect( - CGRect(x: w(stages[0]), y: 0, width: w(stages[1]) - w(stages[0]), height: mapSize.height)) - }.fill(color.stages.ii) - Path { path in - path.addRect( - CGRect(x: w(stages[1]), y: 0, width: w(stages[2]) - w(stages[1]), height: mapSize.height)) - }.fill(color.stages.iii) - Path { path in - path.addRect( - CGRect(x: w(stages[2]), y: 0, width: mapSize.width - w(stages[2]), height: mapSize.height) - ) - }.fill(color.stages.iv) - - Path { path in - path.move(to: CGPoint(x: w(stages[0]), y: 0)) - path.addLine(to: CGPoint(x: w(stages[0]), y: mapSize.height)) - path.move(to: CGPoint(x: w(stages[1]), y: 0)) - path.addLine(to: CGPoint(x: w(stages[1]), y: mapSize.height)) - path.move(to: CGPoint(x: w(stages[2]), y: 0)) - path.addLine(to: CGPoint(x: w(stages[2]), y: mapSize.height)) - path.move(to: CGPoint(x: w(stages[0]), y: 0)) - path.closeSubpath() - }.strokedPath(StrokeStyle(lineWidth: lineWidth, dash: [10.0])).stroke(color.foreground) - } - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } -} - -struct MapStages_Previews: PreviewProvider { - static var previews: some View { - MapStages( - mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), - stages: [25.0, 50.0, 75.0]) - } -} diff --git a/Map/MapRenderComponents/MapVertices.swift b/Map/MapRenderComponents/MapVertices.swift deleted file mode 100644 index 24d49b8..0000000 --- a/Map/MapRenderComponents/MapVertices.swift +++ /dev/null @@ -1,90 +0,0 @@ -import SwiftUI - -struct MapVertices: View { - - @Environment(\.colorScheme) var colorScheme - - let mapSize: CGSize - let vertexSize: CGSize - let vertices: [Vertex] - let padding = CGFloat(4.0) - - var color: MapColor { - MapColor.colorForScheme(colorScheme) - } - - var body: some View { - ForEach(vertices, id: \.id) { vertex in - getVertexShape(vertex).fill(color.foreground) - Text(vertex.label).foregroundColor(color.secondary).offset( - CGSize( - width: w(vertex.position.x) + vertexSize.width + padding, - height: h(vertex.position.y))) - } - } - - func h(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } - - func getVertexShape(_ vertex: Vertex) -> Path { - switch vertex.shape { - case .circle: - return Path { path in - path.addEllipse( - in: CGRect( - origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize - )) - } - case .square: - return Path { path in - path.addRect( - CGRect( - x: w(vertex.position.x), y: h(vertex.position.y), width: vertexSize.width, - height: vertexSize.height - )) - } - case .triangle: - return Path { path in - path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) - path.addLine( - to: CGPoint( - x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height) - ) - path.addLine( - to: CGPoint(x: w(vertex.position.x) + vertexSize.width / 2.0, y: h(vertex.position.y))) - path.addLine( - to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) - path.closeSubpath() - } - case .x: - return Path { path in - path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y))) - path.addLine( - to: CGPoint( - x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height) - ) - path.closeSubpath() - path.move(to: CGPoint(x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y))) - path.addLine( - to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) - path.closeSubpath() - }.strokedPath(StrokeStyle(lineWidth: 5.0, lineCap: .butt)) - } - } -} - -struct MapVertices_Previews: PreviewProvider { - static var previews: some View { - MapVertices( - mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), - vertices: [ - Vertex(id: 0, label: "A", position: CGPoint(x: 50.0, y: 50.0)), - Vertex(id: 0, label: "A", position: CGPoint(x: 10.0, y: 20.0)), - ]) - } -} diff --git a/Map/Presentation/Base Components/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker.swift new file mode 100644 index 0000000..c68e90c --- /dev/null +++ b/Map/Presentation/Base Components/EvolutionPicker.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct EvolutionPicker: View { + + @EnvironmentObject private var store: AppStore + + private var selectedEvolution: Binding { + Binding( + get: { store.state.selectedEvolution }, + set: { evolution in + store.send(.selectEvolution(evolution: evolution)) + } + ) + } + + var body: some View { + Picker("Evolution", selection: selectedEvolution) { + ForEach(StageType.types) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + Divider() + ForEach(StageType.characteristics) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + Divider() + ForEach(StageType.properties) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + Divider() + ForEach(StageType.custom) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + }.padding(.horizontal, 8.0).padding(.vertical, 4.0) + } +} + +struct EvolutionPicker_Previews: PreviewProvider { + static var previews: some View { + EvolutionPicker() + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift new file mode 100644 index 0000000..6ba758f --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift @@ -0,0 +1,81 @@ +import SwiftUI + +struct MapAxes: View { + + let mapSize: CGSize + let lineWidth: CGFloat + let evolution: Stage + let stages: [CGFloat] + let stageHeight = CGFloat(100.0) + let padding = CGFloat(5.0) + + var body: some View { + ZStack(alignment: .topLeading) { + + // Axis Lines + Path { path in + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: 0, y: mapSize.height)) + path.addLine(to: CGPoint(x: mapSize.width, y: mapSize.height)) + path.move(to: CGPoint(x: mapSize.width, y: mapSize.height)) + path.closeSubpath() + }.stroke(Color.map.axisColor, lineWidth: lineWidth * 2) + + // Y Labels + Text("Visible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(Angle(degrees: -90.0)) + .offset(CGSize(width: -35.0, height: 0.0)) + Text("Invisible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(Angle(degrees: -90.0)) + .offset(CGSize(width: -40.0, height: mapSize.height - 20)) + + // X Labels + + Text("Uncharted") + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading) + .offset(CGSize(width: 0.0, height: -stageHeight / 4.0)) + Text("Industrialised") + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading) + .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 4.0)) + + Text(evolution.i) + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: w(stages[0]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: 0.0, height: mapSize.height + padding)) + + Text(evolution.ii) + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: w(stages[1]) - w(stages[0]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[0]), height: mapSize.height + padding)) + + Text(evolution.iii) + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: w(stages[2]) - w(stages[1]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[1]), height: mapSize.height + padding)) + + Text(evolution.iv) + .font(.theme.axisLabel) + .foregroundColor(.map.labelColor) + .frame(width: mapSize.width - w(stages[2]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[2]), height: mapSize.height + padding)) + } + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapAxes_Previews: PreviewProvider { + static var previews: some View { + MapAxes( + mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), + evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0] + ).padding(50.0) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapBlockers.swift b/Map/Presentation/Base Components/MapRender/MapBlockers.swift new file mode 100644 index 0000000..d943ba8 --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapBlockers.swift @@ -0,0 +1,43 @@ +import SwiftUI + +struct MapBlockers: View { + + let mapSize: CGSize + let vertexSize: CGSize + let blockers: [Blocker] + + let cornerSize = CGSize(width: 2.0, height: 2.0) + + var body: some View { + ForEach(blockers, id: \.id) { vertex in + Path { path in + path.addRoundedRect( + in: CGRect( + origin: CGPoint( + x: w(vertex.position.x) + 3 * vertexSize.width, + y: h(vertex.position.y) - vertexSize.height * 2 / 3), + size: CGSize(width: vertexSize.width / 2, height: vertexSize.height * 2) + ), cornerSize: cornerSize) + }.fill(Color.map.blockerColor) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapBlockers_Previews: PreviewProvider { + static var previews: some View { + MapBlockers( + mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), + blockers: [ + Blocker(id: 0, position: CGPoint(x: 50.0, y: 50.0)), + Blocker(id: 1, position: CGPoint(x: 10.0, y: 20.0)), + ]) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapEdges.swift b/Map/Presentation/Base Components/MapRender/MapEdges.swift new file mode 100644 index 0000000..3814495 --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapEdges.swift @@ -0,0 +1,79 @@ +import SwiftUI + +struct MapEdges: View { + + let mapSize: CGSize + let lineWidth: CGFloat + let vertexSize: CGSize + let edges: [MapEdge] + + let arrowheadSize = CGFloat(10.0) + + var body: some View { + ForEach(edges, id: \.id) { edge in + Path { path in + + // First we transform edges from percentage to map coordinates + let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) + let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) + + let slope = (destination.y - origin.y) / (destination.x - origin.x) + let angle = atan(slope) + let multiplier = CGFloat(slope < 0 ? -1.0 : 1.0) + let upperAngle = angle - CGFloat.pi / 4.0 + let lowerAngle = angle + CGFloat.pi / 4.0 + + let offsetOrigin = CGPoint( + x: origin.x + multiplier * (vertexSize.width / 2.0) * cos(angle), + y: origin.y + multiplier * (vertexSize.height / 2.0) * sin(angle)) + let offsetDestination = CGPoint( + x: destination.x - multiplier * (vertexSize.width / 2.0) * cos(angle), + y: destination.y - multiplier * (vertexSize.height / 2.0) * sin(angle)) + + path.move(to: offsetOrigin) + path.addLine(to: offsetDestination) + + if edge.arrowhead { + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) + } + path.move(to: offsetDestination) + path.closeSubpath() + }.applying( + CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) + ).stroke(Color.map.vertexColor, lineWidth: lineWidth) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapEdges_Previews: PreviewProvider { + static var previews: some View { + MapEdges( + mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, + vertexSize: CGSize(width: 25.0, height: 25.0), + edges: [ + MapEdge( + id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2), + arrowhead: true) + ]) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapNotes.swift b/Map/Presentation/Base Components/MapRender/MapNotes.swift new file mode 100644 index 0000000..f35b3fe --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapNotes.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct MapNotes: View { + + let mapSize: CGSize + let lineWidth: CGFloat + let notes: [Note] + + let maxWidth = 400.0 + + + var body: some View { + ForEach(notes, id: \.id) { note in + Text(note.text.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.axisLabel) + .padding(2.0) + .background(.white) + .foregroundColor(.map.labelColor) + .border(Color.map.vertexColor, width: lineWidth) + .frame(minWidth: 16.0, maxWidth: maxWidth, alignment: .topLeading) + .offset( + CGSize( + width: w(note.position.x), + height: h(note.position.y) + ) + ) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapNotes_Previews: PreviewProvider { + static var previews: some View { + MapNotes( + mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, + notes: [ + Note(id: 0, position: CGPoint(x: 50.0, y: 50.0), text: "Notes can have a lot more text, so we need to make sure that they're resized correctly"), + ]) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapOpportunities.swift b/Map/Presentation/Base Components/MapRender/MapOpportunities.swift new file mode 100644 index 0000000..7fcadff --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapOpportunities.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct MapOpportunities: View { + + let mapSize: CGSize + let lineWidth: CGFloat + let vertexSize: CGSize + let opportunities: [Opportunity] + + let arrowheadSize = CGFloat(10.0) + + var body: some View { + ForEach(opportunities, id: \.id) { edge in + Path { path in + + // First we transform edges from percentage to map coordinates + let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) + let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) + + let multiplier = CGFloat(edge.destination.x > edge.origin.x ? 1.0 : -1.0) + let upperAngle = -CGFloat.pi / 4.0 + let lowerAngle = CGFloat.pi / 4.0 + + let offsetOrigin = CGPoint(x: origin.x + multiplier * (vertexSize.width / 2.0), y: origin.y) + let offsetDestination = CGPoint( + x: destination.x - multiplier * (vertexSize.width / 2.0), y: destination.y) + + path.move(to: offsetOrigin) + path.addLine(to: offsetDestination) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) + + path.move(to: offsetDestination) + path.closeSubpath() + }.applying( + CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) + ).strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0])).stroke(Color.map.opportunityColor) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapOpportunities_Previews: PreviewProvider { + static var previews: some View { + MapOpportunities( + mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, + vertexSize: CGSize(width: 25.0, height: 25.0), + opportunities: [ + Opportunity(id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2)) + ]) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapStages.swift b/Map/Presentation/Base Components/MapRender/MapStages.swift new file mode 100644 index 0000000..0fc8f48 --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapStages.swift @@ -0,0 +1,52 @@ +import SwiftUI +import Patterns + +struct MapStages: View { + + let mapSize: CGSize + let lineWidth: CGFloat + let stages: [CGFloat] + let opacity = 0.1 + + var body: some View { + ZStack(alignment: .topLeading) { + PatternView(design: .constant(.stitch), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground) + .frame(width: w(stages[0]), height: mapSize.height) + PatternView(design: .constant(.shingles), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground) + .offset(CGSize(width: w(stages[0]), height: 0)) + .frame(width: w(stages[1]) - w(stages[0]), height: mapSize.height) + PatternView(design: .constant(.shadowGrid), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground) + .offset(CGSize(width: w(stages[1]), height: 0)) + .frame(width: w(stages[2]) - w(stages[1]), height: mapSize.height) + PatternView(design: .constant(.wicker), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground) + .offset(CGSize(width: w(stages[2]), height: 0)) + .frame(width: mapSize.width - w(stages[2]), height: mapSize.height) + + Path { path in + path.move(to: CGPoint(x: w(stages[0]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[0]), y: mapSize.height)) + path.closeSubpath() + path.move(to: CGPoint(x: w(stages[1]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[1]), y: mapSize.height)) + path.closeSubpath() + path.move(to: CGPoint(x: w(stages[2]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[2]), y: mapSize.height)) + path.closeSubpath() + path.move(to: CGPoint(x: w(stages[0]), y: 0)) + path.closeSubpath() + }.strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0, 18.0])).stroke(Color.map.axisColor) + } + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapStages_Previews: PreviewProvider { + static var previews: some View { + MapStages( + mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(0.5), + stages: [25.0, 50.0, 75.0]) + } +} diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift new file mode 100644 index 0000000..74cac6d --- /dev/null +++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct MapVertices: View { + + let mapSize: CGSize + let vertexSize: CGSize + let vertices: [Vertex] + let padding = CGFloat(5.0) + + var body: some View { + ZStack(alignment: .topLeading) { + ForEach(vertices, id: \.id) { vertex in + getVertexShape(vertex).fill(Color.map.vertexColor) + Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.vertexLabel) + .foregroundColor(.map.labelColor) + .shadow(color: .white, radius: 0, x: -0.5, y: -0.5) + .shadow(color: .white, radius: 0, x: 0.5, y: 0.5) + .offset( + CGSize( + width: w(vertex.position.x) + vertexSize.width + padding, + height: h(vertex.position.y) + 7.0)) + } + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } + + func getVertexShape(_ vertex: Vertex) -> Path { + switch vertex.shape { + case .circle: + return Path { path in + path.addEllipse( + in: CGRect( + origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize + )) + } + case .square: + return Path { path in + path.addRect( + CGRect( + x: w(vertex.position.x), y: h(vertex.position.y), width: vertexSize.width, + height: vertexSize.height + )) + } + case .triangle: + return Path { path in + path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) + path.addLine( + to: CGPoint( + x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height) + ) + path.addLine( + to: CGPoint(x: w(vertex.position.x) + vertexSize.width / 2.0, y: h(vertex.position.y))) + path.addLine( + to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) + path.closeSubpath() + } + case .x: + return Path { path in + path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y))) + path.addLine( + to: CGPoint( + x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height) + ) + path.closeSubpath() + path.move(to: CGPoint(x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y))) + path.addLine( + to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) + path.closeSubpath() + }.strokedPath(StrokeStyle(lineWidth: 2.0, lineCap: .butt)) + } + } +} + +struct MapVertices_Previews: PreviewProvider { + static var previews: some View { + MapVertices( + mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), + vertices: [ + Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)), + Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square), + Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle), + Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x), + ]) + } +} diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift new file mode 100644 index 0000000..f3838a6 --- /dev/null +++ b/Map/Presentation/Base Components/MapTextEditor.swift @@ -0,0 +1,154 @@ +import Cocoa +import SwiftUI + +class MapTextEditorController: NSViewController { + + @Binding var text: String + let onChange: () -> Void + + private let vertexRegex = MapParsingPatterns.vertex + private let edgeRegex = MapParsingPatterns.edge + private let blockerRegex = MapParsingPatterns.blocker + private let opportunityRegex = MapParsingPatterns.opportunity + private let noteRegex = MapParsingPatterns.note + private let stageRegex = MapParsingPatterns.stage + + private let changeDebouncer: Debouncer = Debouncer(seconds: 1) + + init(text: Binding, onChange: @escaping () -> Void) { + self._text = text + self.onChange = onChange + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let scrollView = NSTextView.scrollableTextView() + let textView = scrollView.documentView as! NSTextView + + scrollView.translatesAutoresizingMaskIntoConstraints = false + + textView.allowsUndo = true + textView.delegate = self + textView.textStorage?.delegate = self + textView.string = self.text + textView.isEditable = true + textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) + self.view = scrollView + } + + override func viewDidAppear() { + self.view.window?.makeFirstResponder(self.view) + } +} + +extension MapTextEditorController: NSTextViewDelegate { + + func textDidChange(_ obj: Notification) { + if let textField = obj.object as? NSTextView { + self.text = textField.string + + + changeDebouncer.debounce { + DispatchQueue.main.async { + self.onChange() + } + } + } + } + + func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool + { + let range = Range(shouldChangeTextIn, in: view.string) + let target = view.string[range!] + + if target == "--" { + return false + } + + return true + } +} + +extension MapTextEditorController: NSTextStorageDelegate { + + override func textStorageDidProcessEditing(_ obj: Notification) { + if let textStorage = obj.object as? NSTextStorage { + self.colorizeText(textStorage: textStorage) + } + } + + private func colorizeText(textStorage: NSTextStorage) { + let range = NSMakeRange(0, textStorage.length) + var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 4)) + } + + matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) + let arrowRange = match.range(at: 2) + textStorage.addAttributes( + [.foregroundColor: NSColor.syntax.symbol], + range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3)) + } + + matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 4)) + } + + matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) + } + + matches = noteRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) + } + + matches = stageRegex.matches(in: textStorage.string, options: [], range: range) + + for match in matches { + textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) + textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) + } + } +} + +struct MapTextEditor: NSViewControllerRepresentable { + + @Binding var text: String + var onChange: () -> Void = {} + + func makeNSViewController( + context: NSViewControllerRepresentableContext + ) -> MapTextEditorController { + return MapTextEditorController(text: $text, onChange: onChange) + } + + func updateNSViewController( + _ nsViewController: MapTextEditorController, + context: NSViewControllerRepresentableContext + ) {} +} diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift new file mode 100644 index 0000000..b12dabc --- /dev/null +++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift @@ -0,0 +1,63 @@ +import Combine +import CoreData +import CoreGraphics +import SwiftUI + +struct MapRenderView: View { + + @Binding var content: String + @Binding var evolution: StageType + + var stage: Stage { + Stage.stages(evolution) + } + + @State var parsedMap: ParsedMap = ParsedMap.empty + + let mapSize = CGSize(width: 1300.0, height: 1000.0) + + let lineWidth = CGFloat(0.5) + let vertexSize = CGSize(width: 25.0, height: 25.0) + let padding = CGFloat(30.0) + + var body: some View { + ZStack(alignment: .topLeading) { + + Path { path in + path.addRect( + CGRect( + x: -padding, y: -padding, width: mapSize.width + padding * 2, + height: mapSize.height + padding * 4)) + }.fill(.white) + + MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) + MapAxes( + mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: parsedMap.stages) + MapEdges( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) + MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers) + MapVertices(mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices) + MapOpportunities( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, + opportunities: parsedMap.opportunities) + MapNotes( + mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes) + }.frame( + width: mapSize.width, + height: mapSize.height + 2 * padding, alignment: .topLeading + ).onAppear { + self.parsedMap = Map.parse(content: content) + }.padding(padding).onChange(of: content) { newState in + self.parsedMap = Map.parse(content: newState) + } + } +} + +struct MapRenderView_Previews: PreviewProvider { + static var previews: some View { + MapRenderView( + content: Binding.constant(""), evolution: Binding.constant(StageType.general) + ).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Presentation/Screens/EmptyMapDetailScreen.swift b/Map/Presentation/Screens/EmptyMapDetailScreen.swift new file mode 100644 index 0000000..f3c6d75 --- /dev/null +++ b/Map/Presentation/Screens/EmptyMapDetailScreen.swift @@ -0,0 +1,16 @@ +import CoreData +import SwiftUI + +struct EmptyMapDetailScreen: View { + + var body: some View { + Text("Select a map from the left hand side, or click on + to create one.") + } +} + +struct DefaultMapView_Previews: PreviewProvider { + static var previews: some View { + EmptyMapDetailScreen().environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Presentation/Screens/MapDetailScreen.swift b/Map/Presentation/Screens/MapDetailScreen.swift new file mode 100644 index 0000000..a5c32fd --- /dev/null +++ b/Map/Presentation/Screens/MapDetailScreen.swift @@ -0,0 +1,72 @@ +import Combine +import CoreData +import SwiftUI + +struct MapDetailScreen: View { + @Environment(\.managedObjectContext) private var viewContext + + @EnvironmentObject var store: AppStore + + @ObservedObject var map: Map + + @State var title: String + @State var content: String + + var body: some View { + if map.uuid != nil { + VSplitView { + VStack { + HStack { + TextField( + "Title", text: $title, onCommit: saveModel + ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding( + .leading, 4.0) + Button(action: saveText) { + Image(systemName: "doc.text") + }.padding(.vertical, 4.0).padding(.leading, 4.0) + Button(action: saveImage) { + Image(systemName: "photo") + }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0) + } + EvolutionPicker() + + ZStack(alignment: .topLeading) { + MapTextEditor(text: $content, onChange: saveModel) + .background(Color.ui.background) + .foregroundColor(Color.ui.foreground) + .frame(minHeight: 96.0) + }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius( + 5.0) + }.padding(.horizontal, 8.0) + ScrollView([.horizontal, .vertical]) { + MapRenderView(content: $content, evolution: .constant(store.state.selectedEvolution)) + } + }.onDisappear { + saveModel() + } + } else { + EmptyMapDetailScreen() + } + } + + private func saveModel() { + map.content = content + map.title = title + try? viewContext.save() + } + + private func saveText() { + store.send(.exportMapAsText(map: map)) + } + + private func saveImage() { + store.send(.exportMapAsImage(map: map)) + } +} + +struct MapDetailView_Previews: PreviewProvider { + static var previews: some View { + MapDetailScreen(map: Map(), title: "", content: "").environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Presentation/Windows/MapEditorWindow.swift b/Map/Presentation/Windows/MapEditorWindow.swift new file mode 100644 index 0000000..65316cc --- /dev/null +++ b/Map/Presentation/Windows/MapEditorWindow.swift @@ -0,0 +1,109 @@ +import CoreData +import SwiftUI + +struct MapEditorWindow: View { + @Environment(\.managedObjectContext) private var viewContext + + @EnvironmentObject var store: AppStore + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], + animation: .default) + private var maps: FetchedResults + + var body: some View { + NavigationView { + List { + if maps.count == 0 { + EmptyMapDetailScreen() + } + ForEach(maps) { map in + NavigationLink( + destination: MapDetailScreen(map: map, title: map.title ?? "", content: map.content ?? "") + ) { + HStack { + Text(map.title ?? "Untitled Map") + Spacer() + Text(mapFormatter.string(from: (map.createdAt ?? Date()))) + .font(.caption) + .padding(.vertical, 2.0) + .padding(.horizontal, 4.0) + .background(Color.accentColor) + .foregroundColor(Color.black) + .cornerRadius(2.0) + }.padding(.leading, 8.0) + }.contextMenu { + Button( + action: { store.send(.deleteMap(map: map)) }, + label: { + Image(systemName: "trash") + Text("Delete") + }) + } + } + .onDelete(perform: deleteMaps) + }.frame(minWidth: 250.0, alignment: .leading) + .toolbar { + HStack { + Button(action: toggleSidebar) { + Label("Toggle Sidebar", systemImage: "sidebar.left") + } + Button(action: addMap) { + Label("Add Map", systemImage: "plus") + } + } + } + EmptyMapDetailScreen() + } + } + + private func toggleSidebar() { + NSApp.keyWindow?.firstResponder?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil) + } + + private func addMap() { + withAnimation { + let newMap = Map(context: viewContext) + newMap.uuid = UUID() + newMap.createdAt = Date() + newMap.title = "Map \(newMap.createdAt!.format())" + newMap.content = "" + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + private func deleteMaps(offsets: IndexSet) { + + withAnimation { + offsets.map { maps[$0] }.forEach(viewContext.delete) + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +private let mapFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .none + return formatter +}() + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + MapEditorWindow().environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/State/AppState.swift b/Map/State/AppState.swift deleted file mode 100644 index c261725..0000000 --- a/Map/State/AppState.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Cocoa -import Foundation -import SwiftUI - -struct AppState { - var selectedEvolution: StageType = .general -} - -enum AppAction { - case selectEvolution(evolution: StageType) - case exportMapAsImage(map: Map) - case exportMapAsText(map: Map) - case deleteMap(map: Map) -} - -func appStateReducer(state: inout AppState, action: AppAction) { - - switch action { - - case .selectEvolution(let evolution): - state.selectedEvolution = evolution - - case .exportMapAsImage(let map): - let window = NSWindow( - contentRect: .init( - origin: .zero, - size: .init( - width: NSScreen.main!.frame.width, - height: NSScreen.main!.frame.height)), - styleMask: [.closable], - backing: .buffered, - defer: false) - - window.title = map.title ?? "Untitled Map" - window.isOpaque = true - window.center() - window.isMovableByWindowBackground = true - window.makeKeyAndOrderFront(nil) - - let renderView = MapRenderView( - content: Binding.constant(map.content ?? ""), - evolution: Binding.constant(Stage.stages(state.selectedEvolution))) - - let view = NSHostingView(rootView: renderView) - window.contentView = view - - let imageRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds)! - view.cacheDisplay(in: view.bounds, to: imageRepresentation) - let image = NSImage(cgImage: imageRepresentation.cgImage!, size: view.bounds.size) - - let dialog = NSSavePanel() - - dialog.title = "Save Map" - dialog.showsResizeIndicator = false - dialog.canCreateDirectories = true - dialog.showsHiddenFiles = false - dialog.allowedFileTypes = ["png"] - dialog.nameFieldStringValue = map.title ?? "Untitled Map" - - if dialog.runModal() == NSApplication.ModalResponse.OK { - let result = dialog.url - - if result != nil { - - image.writePNG(toURL: result!) - print("saved at \(result!)") - } - } else { - print("Cancel") - } - window.orderOut(nil) - - case .exportMapAsText(let map): - let dialog = NSSavePanel() - - dialog.title = "Save Map Text" - dialog.showsResizeIndicator = false - dialog.canCreateDirectories = true - dialog.showsHiddenFiles = false - dialog.allowedFileTypes = ["txt"] - dialog.nameFieldStringValue = map.title ?? "Untitled Map" - - if let content = map.content { - - if dialog.runModal() == NSApplication.ModalResponse.OK { - let result = dialog.url - - if let result = result { - try? content.write(to: result, atomically: true, encoding: String.Encoding.utf8) - } - } else { - print("Cancel") - } - } - case .deleteMap(let map): - let context = PersistenceController.shared.container.viewContext - context.delete(map) - - try? context.save() - } -} - -typealias AppStore = Store diff --git a/Map/State/Persistence.swift b/Map/State/Persistence.swift deleted file mode 100644 index 1eb09e8..0000000 --- a/Map/State/Persistence.swift +++ /dev/null @@ -1,42 +0,0 @@ -import CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newMap = Map(context: viewContext) - newMap.uuid = UUID() - newMap.createdAt = Date() - newMap.title = "Map \(newMap.createdAt!.format())" - newMap.content = "" - } - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Map") - - container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - container.viewContext.automaticallyMergesChangesFromParent = true - - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/Map/State/Stage.swift b/Map/State/Stage.swift deleted file mode 100644 index 26b1929..0000000 --- a/Map/State/Stage.swift +++ /dev/null @@ -1,195 +0,0 @@ -struct Stage { - let i: String - let ii: String - let iii: String - let iv: String - - static func stages(_ type: StageType) -> Stage { - switch type { - case .general: - return Stage( - i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)") - case .practice: - return Stage( - i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") - case .data: - return Stage( - i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") - case .knowledge: - return Stage( - i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") - case .ubiquity: - return Stage( - i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing", - iv: "Widespread in the applicable market / ecosystem") - case .certainty: - return Stage( - i: "Poorly Understood / exploring the unknown", - ii: "Rapid Increase In Learning / discovery becomes refining", - iii: "Rapid increase in use / increasing fit for purpose", - iv: "Commonly understood (in terms of use)") - case .publicationTypes: - return Stage( - i: - "Describe the wonder of the thing / the discovery of some marvel / a new land / an unknown frontier", - ii: - "Focused on build / construct / awareness and learning / many models of explanation / no accepted forms / a wild west", - iii: - "Maintenance / operations / installation / comparison between competing forms / feature analysis", - iv: "Focused on use / increasingly an accepted, almost invisible component") - case .market: - return Stage( - i: "Undefined Market", - ii: "Forming Market / an array of competing forms and models of understanding", - iii: "Growing Market / consolidation to a few competing but more accepted forms", - iv: "Mature Market / stabilised to an accepted form") - case .knowledgeManagement: - return Stage( - i: "Uncertain", ii: "Learning on use / focused on testing prediction", - iii: "Learning on operation / using prediction / verification", iv: "Known / accepted") - case .marketPerception: - return Stage( - i: "Chaotic (non-linear) / domain of the \"crazy\"", ii: "Domain of \"experts\"", - iii: "Increasing expectation of use / domain of \"professionals\"", - iv: "Ordered (appearance of being linear) / trivial / formula to be applied") - case .userPerception: - return Stage( - i: "Different / confusing / exciting / surprising / dangerous", - ii: "Leading edge / emerging / unceirtanty over results", - iii: "Increasingly common / disappointed if not used or available / feeling left behind", - iv: "Standard / expected / feeling of shock if not used") - case .perceptionInIndustry: - return Stage( - i: "Future source of competitive advantage / unpredictable / unknown", - ii: "Seen as a scompetitive advantage / a differential / ROI / case examples", - iii: "Advantage through implementation / features / this model is better than that", - iv: "Cost of doing business / accepted / specific defined models") - case .focusOfValue: - return Stage( - i: "High future worth but immediate investment", - ii: "Seeking ways to profit and a ROI / seeking confirmation of value", - iii: - "High profitability per unit / a valuable model / a feeling of understanding / focus on exploitation", - iv: - "High volume / reducing margin / important but invisible / an essential component of something more complex" - ) - case .understanding: - return Stage( - i: "Poorly Understood / unpredictable", - ii: "Increasing understanding / development of measures", - iii: "Increasing education / constant refinement of needs / measures", - iv: "Believed to be well defined / stable / measurable") - case .comparison: - return Stage( - i: "Constantly changing / a differential / unstable", - ii: "Learning from others / testing the water / some evidential support", - iii: "Competing models / feature difference / evidential support", - iv: "Essential / any advantage is operational / accepted norm") - case .failure: - return Stage( - i: "High / tolerated / assumed to be wrong", - ii: "Moderate / unsurprising if wrong but disappointed", - iii: - "Not tolerated / focus on constant improvement / assumed to be in the right direction / resistance to changing the model", - iv: "Surprised by failure / focus on operational efficiency") - case .marketAction: - return Stage( - i: "Gambling / driven by gut", ii: "Exploring a \"found\" value", - iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") - case .efficiency: - return Stage( - i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", - iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") - case .decisionDrivers: - return Stage( - i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", - iv: "Previous Experience") - case .behavior: - return Stage( - i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", - iv: "Known / common usage") - } - } - - static func title(_ type: StageType) -> String { - switch type { - case .general: - return "Activities" - case .practice: - return "Practice" - case .data: - return "Data" - case .knowledge: - return "Knowledge" - case .ubiquity: - return "Ubiquity" - case .certainty: - return "Certainty" - case .publicationTypes: - return "Publication Types" - case .market: - return "Market" - case .knowledgeManagement: - return "Knowledge Management" - case .marketPerception: - return "Market Perception" - case .userPerception: - return "User Perception" - case .perceptionInIndustry: - return "Perception In Industry" - case .focusOfValue: - return "Focus Of Value" - case .understanding: - return "Understanding" - case .comparison: - return "Comparison" - case .failure: - return "Failure" - case .marketAction: - return "Market Action" - case .efficiency: - return "Efficiency" - case .decisionDrivers: - return "Decision Drivers" - case .behavior: - return "Behavior" - } - } -} - -enum StageType: String, CaseIterable, Identifiable { - case general - case practice - case data - case knowledge - - case ubiquity - case certainty - case publicationTypes - - case market - case knowledgeManagement - case marketPerception - case userPerception - case perceptionInIndustry - case focusOfValue - case understanding - case comparison - case failure - case marketAction - case efficiency - case decisionDrivers - - case behavior - - var id: String { self.rawValue } - - static let types: [StageType] = [.general, .practice, .data, .knowledge] - static let characteristics: [StageType] = [.ubiquity, .certainty, .publicationTypes] - static let properties: [StageType] = [ - .market, .knowledgeManagement, .marketPerception, .userPerception, - .perceptionInIndustry, .focusOfValue, .understanding, .comparison, .failure, - .marketAction, .efficiency, .decisionDrivers, - ] - static let custom: [StageType] = [.behavior] -} diff --git a/Map/State/Store.swift b/Map/State/Store.swift deleted file mode 100644 index 7860f33..0000000 --- a/Map/State/Store.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -final class Store: ObservableObject { - @Published private(set) var state: State - - private let reducer: Reducer - - init(initialState: State, reducer: @escaping Reducer) { - self.state = initialState - self.reducer = reducer - } - - func send(_ action: Action) { - reducer(&state, action) - } -} - -typealias Reducer = (inout State, Action) -> Void diff --git a/Map/Views/ContentView.swift b/Map/Views/ContentView.swift deleted file mode 100644 index 534c14c..0000000 --- a/Map/Views/ContentView.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import CoreData -import SwiftUI - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @EnvironmentObject var store: AppStore - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], - animation: .default) - private var maps: FetchedResults - - var body: some View { - NavigationView { - List { - if maps.count == 0 { - DefaultMapView() - } - ForEach(maps) { map in - NavigationLink( - destination: MapDetailView(map: map, title: map.title ?? "", content: map.content ?? "") - ) { - HStack { - Text(map.title ?? "Untitled Map") - Spacer() - Text(mapFormatter.string(from: (map.createdAt ?? Date()))) - .font(.caption) - .padding(.vertical, 2.0) - .padding(.horizontal, 4.0) - .background(Color.accentColor) - .foregroundColor(Color.black) - .cornerRadius(2.0) - }.padding(.leading, 8.0) - }.contextMenu { - Button( - action: { store.send(.deleteMap(map: map)) }, - label: { - Image(systemName: "trash") - Text("Delete") - }) - } - } - .onDelete(perform: deleteMaps) - }.frame(minWidth: 250.0, alignment: .leading) - .toolbar { - HStack { - Button(action: toggleSidebar) { - Label("Toggle Sidebar", systemImage: "sidebar.left") - } - Button(action: addMap) { - Label("Add Map", systemImage: "plus") - } - } - } - DefaultMapView() - } - } - - private func toggleSidebar() { - NSApp.keyWindow?.firstResponder?.tryToPerform( - #selector(NSSplitViewController.toggleSidebar(_:)), with: nil) - } - - private func addMap() { - withAnimation { - let newMap = Map(context: viewContext) - newMap.uuid = UUID() - newMap.createdAt = Date() - newMap.title = "Map \(newMap.createdAt!.format())" - newMap.content = "" - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteMaps(offsets: IndexSet) { - - withAnimation { - offsets.map { maps[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let mapFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .none - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment( - \.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Views/DefaultMapView.swift b/Map/Views/DefaultMapView.swift deleted file mode 100644 index 1f624d2..0000000 --- a/Map/Views/DefaultMapView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import CoreData -import SwiftUI - -struct DefaultMapView: View { - - var body: some View { - Text("Select a map from the left hand side, or click on + to create one.") - } -} - -struct DefaultMapView_Previews: PreviewProvider { - static var previews: some View { - DefaultMapView().environment( - \.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Views/EvolutionPicker.swift b/Map/Views/EvolutionPicker.swift deleted file mode 100644 index 815f575..0000000 --- a/Map/Views/EvolutionPicker.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// EvolutionPicker.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/4/21. -// - -import SwiftUI - -struct EvolutionPicker: View { - - @EnvironmentObject private var store: AppStore - - private var selectedEvolution: Binding { - Binding( - get: { store.state.selectedEvolution }, - set: { evolution in - store.send(.selectEvolution(evolution: evolution)) - } - ) - } - - var body: some View { - Picker("Evolution", selection: selectedEvolution) { - ForEach(StageType.types) { stage in - Text(Stage.title(stage)).tag(stage).padding(4.0) - } - Divider() - ForEach(StageType.characteristics) { stage in - Text(Stage.title(stage)).tag(stage).padding(4.0) - } - Divider() - ForEach(StageType.properties) { stage in - Text(Stage.title(stage)).tag(stage).padding(4.0) - } - Divider() - ForEach(StageType.custom) { stage in - Text(Stage.title(stage)).tag(stage).padding(4.0) - } - }.padding(.horizontal, 8.0).padding(.vertical, 4.0) - } -} - -struct EvolutionPicker_Previews: PreviewProvider { - static var previews: some View { - EvolutionPicker() - } -} diff --git a/Map/Views/MapApp.swift b/Map/Views/MapApp.swift deleted file mode 100644 index a0e2d60..0000000 --- a/Map/Views/MapApp.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// MapApp.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI - -@main -struct MapApp: App { - let persistenceController = PersistenceController.shared - - var body: some Scene { - WindowGroup { - ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(AppStore(initialState: AppState(), reducer: appStateReducer)) - }.windowStyle(HiddenTitleBarWindowStyle()).commands { - SidebarCommands() - } - } -} diff --git a/Map/Views/MapDetail.swift b/Map/Views/MapDetail.swift deleted file mode 100644 index bebe3a1..0000000 --- a/Map/Views/MapDetail.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import Combine -import CoreData -import SwiftUI - -class SaveTimer { - let currentTimePublisher = Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default) - let cancellable: AnyCancellable? - - init() { - self.cancellable = currentTimePublisher.connect() as? AnyCancellable - } - - deinit { - self.cancellable?.cancel() - } -} - -let timer = SaveTimer() - -struct MapDetailView: View { - @Environment(\.managedObjectContext) private var viewContext - @Environment(\.colorScheme) var colorScheme - - @EnvironmentObject var store: AppStore - - @ObservedObject var map: Map - - private var mapColor: MapColor { - MapColor.colorForScheme(colorScheme) - } - - @State var title: String - @State var content: String - - var body: some View { - if map.uuid != nil { - VSplitView { - VStack { - HStack { - TextField( - "Title", text: $title - ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding( - .leading, 4.0) - Button(action: saveText) { - Image(systemName: "doc.text") - }.padding(.vertical, 4.0).padding(.leading, 4.0) - Button(action: saveImage) { - Image(systemName: "photo") - }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0) - } - EvolutionPicker() - - ZStack(alignment: .topLeading) { - MapTextEditor(text: $content, colorScheme: colorScheme) - .background(mapColor.background) - .foregroundColor(mapColor.foreground) - .frame(minHeight: 96.0) - }.padding(.top, 8.0).padding(.leading, 8.0).background(mapColor.background).cornerRadius( - 5.0) - }.padding(.horizontal, 8.0) - ScrollView([.horizontal, .vertical]) { - SlowMapRender( - content: content, evolution: Stage.stages(store.state.selectedEvolution), - colorScheme: colorScheme) - }.background(mapColor.background) - }.onReceive(timer.currentTimePublisher) { _ in - saveModel() - }.onDisappear { - saveModel() - } - } else { - DefaultMapView() - } - } - - private func saveModel() { - map.content = content - map.title = title - try? viewContext.save() - } - - private func saveText() { - store.send(.exportMapAsText(map: map)) - } - - private func saveImage() { - store.send(.exportMapAsImage(map: map)) - } -} - -struct MapDetailView_Previews: PreviewProvider { - static var previews: some View { - MapDetailView(map: Map(), title: "", content: "").environment( - \.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Views/MapRender.swift b/Map/Views/MapRender.swift deleted file mode 100644 index aa97e22..0000000 --- a/Map/Views/MapRender.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import Combine -import CoreData -import CoreGraphics -import SwiftUI - -struct MapRenderView: View { - - @Environment(\.colorScheme) var colorScheme - - @Binding var content: String - @Binding var evolution: Stage - - @State var parsedMap: ParsedMap = ParsedMap.empty - - let mapSize = CGSize(width: 1300.0, height: 1000.0) - - let lineWidth = CGFloat(1.0) - let vertexSize = CGSize(width: 25.0, height: 25.0) - let padding = CGFloat(30.0) - - var body: some View { - ZStack(alignment: .topLeading) { - - Path { path in - path.addRect( - CGRect( - x: -padding, y: -padding, width: mapSize.width + padding * 2, - height: mapSize.height + padding * 4)) - }.fill(MapColor.colorForScheme(colorScheme).background) - - MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) - MapAxes( - mapSize: mapSize, lineWidth: lineWidth, evolution: evolution, stages: parsedMap.stages) - MapOpportunities( - mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, - opportunities: parsedMap.opportunities) - MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers) - MapVertices(mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices) - MapEdges( - mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) - }.frame( - width: mapSize.width, - height: mapSize.height + 2 * padding, alignment: .topLeading - ).onAppear { - self.parsedMap = Map.parse(content: content) - }.padding(padding).onChange(of: content) { newState in - self.parsedMap = Map.parse(content: newState) - } - } -} - -struct MapRenderView_Previews: PreviewProvider { - static var previews: some View { - MapRenderView( - content: Binding.constant(""), evolution: Binding.constant(Stage.stages(.general)) - ).environment( - \.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Views/MapTextEditor.swift b/Map/Views/MapTextEditor.swift deleted file mode 100644 index 4fbff82..0000000 --- a/Map/Views/MapTextEditor.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Cocoa -import SwiftUI - -class MapTextEditorController: NSViewController { - - @Binding var text: String - var colorScheme: ColorScheme - - private let vertexRegex = MapParsingPatterns.vertex - private let edgeRegex = MapParsingPatterns.edge - private let blockerRegex = MapParsingPatterns.blocker - private let opportunityRegex = MapParsingPatterns.opportunity - private let stageRegex = MapParsingPatterns.stage - - private let debouncer: Debouncer = Debouncer(seconds: 0.2) - - init(text: Binding, colorScheme: ColorScheme) { - self._text = text - self.colorScheme = colorScheme - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func loadView() { - let scrollView = NSTextView.scrollableTextView() - let textView = scrollView.documentView as! NSTextView - - scrollView.translatesAutoresizingMaskIntoConstraints = false - - textView.allowsUndo = true - textView.delegate = self - textView.textStorage?.delegate = self - textView.string = self.text - textView.isEditable = true - textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) - self.view = scrollView - } - - override func viewDidAppear() { - self.view.window?.makeFirstResponder(self.view) - } - - func updateColorScheme(_ colorScheme: ColorScheme) { - self.colorScheme = colorScheme - } -} - -extension MapTextEditorController: NSTextViewDelegate { - - func textDidChange(_ obj: Notification) { - if let textField = obj.object as? NSTextView { - self.text = textField.string - } - } - - func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool - { - let range = Range(shouldChangeTextIn, in: view.string) - let target = view.string[range!] - - if target == "--" { - return false - } - - return true - } -} - -extension MapTextEditorController: NSTextStorageDelegate { - override func textStorageDidProcessEditing(_ obj: Notification) { - if let textStorage = obj.object as? NSTextStorage { - debouncer.debounce { - DispatchQueue.main.async { - self.colorizeText(textStorage: textStorage) - } - } - } - } - - private func colorizeText(textStorage: NSTextStorage) { - let range = NSMakeRange(0, textStorage.length) - var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) - let colors = MapColor.colorForScheme(colorScheme) - - for match in matches { - textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1)) - textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2)) - textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 3)) - textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 4)) - } - - matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) - - for match in matches { - textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1)) - let arrowRange = match.range(at: 2) - textStorage.addAttributes( - [.foregroundColor: colors.syntax.symbol], - range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) - textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 3)) - } - - matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) - - for match in matches { - textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) - textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2)) - textStorage.addAttributes([.foregroundColor: colors.syntax.symbol], range: match.range(at: 3)) - textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 4)) - } - - matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) - - for match in matches { - textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) - textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2)) - } - - matches = stageRegex.matches(in: textStorage.string, options: [], range: range) - - for match in matches { - textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) - textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2)) - } - } -} - -struct MapTextEditor: NSViewControllerRepresentable { - - @Binding var text: String - let colorScheme: ColorScheme - - func makeNSViewController( - context: NSViewControllerRepresentableContext - ) -> MapTextEditorController { - return MapTextEditorController(text: $text, colorScheme: colorScheme) - } - - func updateNSViewController( - _ nsViewController: MapTextEditorController, - context: NSViewControllerRepresentableContext - ) { - nsViewController.updateColorScheme(context.environment.colorScheme) - } -} diff --git a/Map/Views/SlowMapRender.swift b/Map/Views/SlowMapRender.swift deleted file mode 100644 index b7cd842..0000000 --- a/Map/Views/SlowMapRender.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Cocoa -import SwiftUI - -class SlowMapRenderController: NSViewController { - - var content: String - private var contentBinding: Binding { - Binding( - get: { self.content }, - set: { content in - self.content = content - } - ) - } - - var evolution: Stage - private var evolutionBinding: Binding { - Binding( - get: { self.evolution }, - set: { evolution in - self.evolution = evolution - } - ) - } - private var colorSchemeEnvironment: ObservableColorSchemeEnvironment - - private let debouncer: Debouncer = Debouncer(seconds: 0.1) - - init(content: String, evolution: Stage, colorScheme: ColorScheme) { - - self.content = content - self.evolution = evolution - self.colorSchemeEnvironment = ObservableColorSchemeEnvironment(colorScheme: colorScheme) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func loadView() { - - let renderView = MapRenderView(content: self.contentBinding, evolution: self.evolutionBinding) - .environmentObject(self.colorSchemeEnvironment) - let hostingView = NSHostingView(rootView: renderView) - - self.view = hostingView - } - - func update(content: String, evolution: Stage, colorScheme: ColorScheme) { - self.debouncer.debounce { - DispatchQueue.main.async { - print("Updating: START") - self.content = content - self.evolution = evolution - self.colorSchemeEnvironment.colorScheme = colorScheme - print("Updating: END") - } - } - } - - private class ObservableColorSchemeEnvironment: ObservableObject { - @Published var colorScheme: ColorScheme - - init(colorScheme: ColorScheme) { - self.colorScheme = colorScheme - } - } -} - -struct SlowMapRender: NSViewControllerRepresentable { - - var content: String - var evolution: Stage - let colorScheme: ColorScheme - - func makeNSViewController( - context: NSViewControllerRepresentableContext - ) -> SlowMapRenderController { - return SlowMapRenderController(content: content, evolution: evolution, colorScheme: colorScheme) - } - - func updateNSViewController( - _ nsViewController: SlowMapRenderController, - context: NSViewControllerRepresentableContext - ) { - nsViewController.update( - content: content, evolution: evolution, colorScheme: context.environment.colorScheme) - } -} -- cgit