aboutsummaryrefslogtreecommitdiff
path: root/Map/Views
diff options
context:
space:
mode:
Diffstat (limited to 'Map/Views')
-rw-r--r--Map/Views/ContentView.swift104
-rw-r--r--Map/Views/DefaultMapView.swift23
-rw-r--r--Map/Views/MapApp.swift21
-rw-r--r--Map/Views/MapDetail.swift101
-rw-r--r--Map/Views/MapRender.swift61
-rw-r--r--Map/Views/MapTextEditor.swift71
-rw-r--r--Map/Views/Stage.swift161
7 files changed, 542 insertions, 0 deletions
diff --git a/Map/Views/ContentView.swift b/Map/Views/ContentView.swift
new file mode 100644
index 0000000..4f55c27
--- /dev/null
+++ b/Map/Views/ContentView.swift
@@ -0,0 +1,104 @@
+//
+// 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
+
+ @FetchRequest(
+ sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)],
+ animation: .default)
+ private var maps: FetchedResults<Map>
+
+ var body: some View {
+ NavigationView {
+ List {
+ if maps.count == 0 {
+ DefaultMapView()
+ }
+ ForEach(maps) { map in
+ NavigationLink(destination: MapDetailView(map: map)) {
+ 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)
+ }
+ }
+ .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")
+ }
+ }
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..66914f1
--- /dev/null
+++ b/Map/Views/DefaultMapView.swift
@@ -0,0 +1,23 @@
+//
+// 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 {
+ MapDetailView(map: Map()).environment(
+ \.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+}
diff --git a/Map/Views/MapApp.swift b/Map/Views/MapApp.swift
new file mode 100644
index 0000000..0f8ae64
--- /dev/null
+++ b/Map/Views/MapApp.swift
@@ -0,0 +1,21 @@
+//
+// 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())
+ }
+}
diff --git a/Map/Views/MapDetail.swift b/Map/Views/MapDetail.swift
new file mode 100644
index 0000000..a8ea3d1
--- /dev/null
+++ b/Map/Views/MapDetail.swift
@@ -0,0 +1,101 @@
+//
+// ContentView.swift
+// Map
+//
+// Created by Ruben Beltran del Rio on 2/1/21.
+//
+
+import CoreData
+import SwiftUI
+
+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)
+ }
+
+ private var title: Binding<String> {
+ Binding(
+ get: { map.title ?? "" },
+ set: { title in
+ map.title = title
+ }
+ )
+ }
+
+ private var content: Binding<String> {
+ Binding(
+ get: { map.content ?? "" },
+ set: { content in
+ map.content = content
+ }
+ )
+ }
+
+ @State private var selectedEvolution = StageType.general
+
+ var body: some View {
+ if map.uuid != nil {
+ VSplitView {
+ VStack {
+ HStack {
+ TextField(
+ "Title", text: title,
+ onCommit: {
+ try? viewContext.save()
+ }
+ ).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)
+ }
+ Picker("Evolution", selection: $selectedEvolution) {
+ ForEach(StageType.allCases) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+
+ ZStack(alignment: .topLeading) {
+ MapTextEditor(text: content).onChange(of: map.content) { _ in
+ try? viewContext.save()
+ }
+ .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]) {
+ MapRenderView(map: map, evolution: Stage.stages(selectedEvolution))
+ }.background(mapColor.background)
+ }
+ } else {
+ DefaultMapView()
+ }
+ }
+
+ private func saveText() {
+ store.send(.exportMapAsText(map: map))
+ }
+
+ private func saveImage() {
+ store.send(.exportMapAsImage(map: map, evolution: selectedEvolution))
+ }
+}
+
+struct MapDetailView_Previews: PreviewProvider {
+ static var previews: some View {
+ MapDetailView(map: Map()).environment(
+ \.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+}
diff --git a/Map/Views/MapRender.swift b/Map/Views/MapRender.swift
new file mode 100644
index 0000000..b35b724
--- /dev/null
+++ b/Map/Views/MapRender.swift
@@ -0,0 +1,61 @@
+//
+// ContentView.swift
+// Map
+//
+// Created by Ruben Beltran del Rio on 2/1/21.
+//
+
+import CoreData
+import CoreGraphics
+import SwiftUI
+
+struct MapRenderView: View {
+
+ @Environment(\.colorScheme) var colorScheme
+
+ @ObservedObject var map: Map
+ let evolution: Stage
+
+ 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 parsedMap: ParsedMap {
+ return map.parse()
+ }
+
+ 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 * 2))
+ }.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, alignment: .topLeading
+ ).padding(padding)
+ }
+}
+
+struct MapRenderView_Previews: PreviewProvider {
+ static var previews: some View {
+ MapDetailView(map: Map()).environment(
+ \.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+}
diff --git a/Map/Views/MapTextEditor.swift b/Map/Views/MapTextEditor.swift
new file mode 100644
index 0000000..7251c59
--- /dev/null
+++ b/Map/Views/MapTextEditor.swift
@@ -0,0 +1,71 @@
+import Cocoa
+import SwiftUI
+
+class MapTextEditorController: NSViewController {
+
+ @Binding var text: String
+
+ init(text: Binding<String>) {
+ self._text = text
+ 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.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
+ }
+ }
+
+ 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
+ }
+}
+
+struct MapTextEditor: NSViewControllerRepresentable {
+
+ @Binding var text: String
+
+ func makeNSViewController(
+ context: NSViewControllerRepresentableContext<MapTextEditor>
+ ) -> MapTextEditorController {
+ return MapTextEditorController(text: $text)
+ }
+
+ func updateNSViewController(
+ _ nsViewController: MapTextEditorController,
+ context: NSViewControllerRepresentableContext<MapTextEditor>
+ ) {
+ }
+}
diff --git a/Map/Views/Stage.swift b/Map/Views/Stage.swift
new file mode 100644
index 0000000..23432ad
--- /dev/null
+++ b/Map/Views/Stage.swift
@@ -0,0 +1,161 @@
+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 Built", 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 Consumption", iii: "Rapidly Increasing Consumption",
+ iv: "Widespread and stabilising")
+ case .certainty:
+ return Stage(
+ i: "Poorly Understood", ii: "Rapid Increase In Learning",
+ iii: "Rapid Increase in Use / fit for purpose", iv: "Commonly understood (in terms of use)")
+ case .publicationTypes:
+ return Stage(
+ i: "Normally describing the wonder of the thing",
+ ii: "Build / construct / awareness and learning",
+ iii: "Maintenance / operations / installation / feature", iv: "Focused on use")
+ case .market:
+ return Stage(
+ i: "Undefined Market", ii: "Forming Market", iii: "Growing Market", iv: "Mature Market")
+ case .knowledgeManagement:
+ return Stage(
+ i: "Uncertain", ii: "Learning on use", iii: "Learning on operation", iv: "Known / accepted")
+ case .marketPerception:
+ return Stage(
+ i: "Chaotic (non-linear)", ii: "Domain of experts", iii: "Increasing expectation of use",
+ iv: "Ordered (appearance of being trivial) / trivial")
+ case .userPerception:
+ return Stage(
+ i: "Different / confusing / exciting / surprising", ii: "Leading edge / emerging",
+ iii: "Increasingly common / disappointed if not used", iv: "Standard / expected")
+ case .perceptionInIndustry:
+ return Stage(
+ i: "Competitive advantage / unpredictable / unknown",
+ ii: "Competitive advantage / ROI / case examples",
+ iii: "Advantage through implementation / features", iv: "Cost of doing business")
+ case .focusOfValue:
+ return Stage(
+ i: "High future worth", ii: "Seeking profit / ROI", iii: "High profitability",
+ iv: "High volume / reducing margin")
+ 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: "Feature difference", iv: "Essential / operational advantage")
+ case .failure:
+ return Stage(
+ i: "High / tolerated / assumed", ii: "Moderate / unsurprising but disappointed",
+ iii: "Not tolerated, focus on constant improvement",
+ iv: "Operational efficiency and surprised by failure")
+ 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 }
+}