From: Ruben Beltran del Rio Date: Wed, 3 Feb 2021 22:53:12 +0000 (+0100) Subject: Add code for first release X-Git-Tag: 1.0.0~1 X-Git-Url: https://git.r.bdr.sh/rbdr/map/commitdiff_plain/5e8ff4850c4827125fe12788dd5b153c4f636f48 Add code for first release --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5e112f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing to Map + +Contributions are welcome! If you find this app helpful and would like to +help, please read on. + +## Why does this app exist? + +I use wardley maps for many things, and I find that most diagram apps +are cumbersome to use for this, and pen and paper is hard to share. + +I created this app inspired by graphviz, hoping to create a language +that allows for the easy creation, modification and sharing of wardley maps. + +### What kind of contributions are welcome? + +Any improvements on the existing features, and bugfixes are welcome. + +If you would like to add a new feature, please add an issue +before submitting code, so we can discuss; also, feel free to fork! + +## How to contribute + +Above All: Be nice, always. + +Befoe opening a merge request: + +* Run `make format` +* Ensure `make lint` shows no warnings or errors +* Make a merge request describing what/why is included in your changes diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..497db7d --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Rubén Beltran del Río + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe56dfc --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +swift_version = 5.3 + +format: + swift format -i -r . + +lint: + swift format -m lint -r . + +docker-build: + docker build --force-rm --build-arg swift_version=$(swift_version) -t doapp/do:$(swift_version) . + +docker-push: docker-build + docker push doapp/do:$(swift_version) + +.PHONY: format lint docker-build docker-push diff --git a/Map.xcodeproj/project.pbxproj b/Map.xcodeproj/project.pbxproj index b7dc2fa..275bb9e 100644 --- a/Map.xcodeproj/project.pbxproj +++ b/Map.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */; }; + B523C74625C9BD3500C44061 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B523C74525C9BD3500C44061 /* CloudKit.framework */; }; + B523C74B25C9C1BA00C44061 /* DefaultMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */; }; + B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C75925C9FD4900C44061 /* MapAxes.swift */; }; + B523C76225CA05A300C44061 /* MapStages.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76125CA05A300C44061 /* MapStages.swift */; }; + B523C76725CA071B00C44061 /* MapVertices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76625CA071B00C44061 /* MapVertices.swift */; }; + B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76B25CA0DFA00C44061 /* MapEdges.swift */; }; + B523C77125CA121300C44061 /* MapBlockers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C77025CA121300C44061 /* MapBlockers.swift */; }; + B523C77625CA181100C44061 /* MapColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C77525CA181100C44061 /* MapColor.swift */; }; + B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C77D25CA294C00C44061 /* MapOpportunities.swift */; }; B526257225C874F9003E73B7 /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526257125C874F9003E73B7 /* MapApp.swift */; }; B526257425C874F9003E73B7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526257325C874F9003E73B7 /* ContentView.swift */; }; B526257625C874FA003E73B7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B526257525C874FA003E73B7 /* Assets.xcassets */; }; @@ -18,9 +28,11 @@ B52625A625C876C3003E73B7 /* MapDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625A525C876C3003E73B7 /* MapDetail.swift */; }; B52625AB25C87909003E73B7 /* Date+format.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625AA25C87909003E73B7 /* Date+format.swift */; }; B52625B025C87C14003E73B7 /* MapRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625AF25C87C14003E73B7 /* MapRender.swift */; }; - B52625B625C87D69003E73B7 /* Binding+unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625B525C87D69003E73B7 /* Binding+unwrap.swift */; }; B52625BB25C884C2003E73B7 /* Map+parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625BA25C884C2003E73B7 /* Map+parse.swift */; }; B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625C525C8BD2A003E73B7 /* Stage.swift */; }; + B539516C25CB0C9300959F72 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539516B25CB0C9200959F72 /* Store.swift */; }; + B539517425CB0CA400959F72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539517325CB0CA400959F72 /* AppState.swift */; }; + B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539518025CB2D7A00959F72 /* MapTextEditor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,6 +53,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+writePNG.swift"; sourceTree = ""; }; + B523C74525C9BD3500C44061 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultMapView.swift; sourceTree = ""; }; + B523C75925C9FD4900C44061 /* MapAxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAxes.swift; sourceTree = ""; }; + B523C76125CA05A300C44061 /* MapStages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapStages.swift; sourceTree = ""; }; + B523C76625CA071B00C44061 /* MapVertices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVertices.swift; sourceTree = ""; }; + B523C76B25CA0DFA00C44061 /* MapEdges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEdges.swift; sourceTree = ""; }; + B523C77025CA121300C44061 /* MapBlockers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapBlockers.swift; sourceTree = ""; }; + B523C77525CA181100C44061 /* MapColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapColor.swift; sourceTree = ""; }; + B523C77D25CA294C00C44061 /* MapOpportunities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapOpportunities.swift; sourceTree = ""; }; B526256E25C874F9003E73B7 /* Map.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Map.app; sourceTree = BUILT_PRODUCTS_DIR; }; B526257125C874F9003E73B7 /* MapApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapApp.swift; sourceTree = ""; }; B526257325C874F9003E73B7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -59,9 +81,11 @@ B52625A525C876C3003E73B7 /* MapDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDetail.swift; sourceTree = ""; }; B52625AA25C87909003E73B7 /* Date+format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+format.swift"; sourceTree = ""; }; B52625AF25C87C14003E73B7 /* MapRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRender.swift; sourceTree = ""; }; - B52625B525C87D69003E73B7 /* Binding+unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+unwrap.swift"; sourceTree = ""; }; B52625BA25C884C2003E73B7 /* Map+parse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Map+parse.swift"; sourceTree = ""; }; B52625C525C8BD2A003E73B7 /* Stage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = ""; }; + B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,6 +93,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B523C74625C9BD3500C44061 /* CloudKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,6 +114,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B523C74425C9BD3500C44061 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B523C74525C9BD3500C44061 /* CloudKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B523C75825C9FD3A00C44061 /* MapRenderComponents */ = { + isa = PBXGroup; + children = ( + B523C75925C9FD4900C44061 /* MapAxes.swift */, + B523C76125CA05A300C44061 /* MapStages.swift */, + B523C76625CA071B00C44061 /* MapVertices.swift */, + B523C76B25CA0DFA00C44061 /* MapEdges.swift */, + B523C77025CA121300C44061 /* MapBlockers.swift */, + B523C77525CA181100C44061 /* MapColor.swift */, + B523C77D25CA294C00C44061 /* MapOpportunities.swift */, + ); + path = MapRenderComponents; + sourceTree = ""; + }; B526256525C874F9003E73B7 = { isa = PBXGroup; children = ( @@ -96,6 +143,7 @@ B526258825C874FA003E73B7 /* MapTests */, B526259325C874FA003E73B7 /* MapUITests */, B526256F25C874F9003E73B7 /* Products */, + B523C74425C9BD3500C44061 /* Frameworks */, ); sourceTree = ""; }; @@ -112,18 +160,15 @@ B526257025C874F9003E73B7 /* Map */ = { isa = PBXGroup; children = ( + B539516A25CB0C7800959F72 /* State */, B52625B425C87D54003E73B7 /* Extensions */, - B526257125C874F9003E73B7 /* MapApp.swift */, - B526257325C874F9003E73B7 /* ContentView.swift */, + B539517925CB0D6100959F72 /* Views */, + B523C75825C9FD3A00C44061 /* MapRenderComponents */, B526257525C874FA003E73B7 /* Assets.xcassets */, - B526257A25C874FA003E73B7 /* Persistence.swift */, B526257F25C874FA003E73B7 /* Info.plist */, B526258025C874FA003E73B7 /* Map.entitlements */, B526257C25C874FA003E73B7 /* Map.xcdatamodeld */, B526257725C874FA003E73B7 /* Preview Content */, - B52625A525C876C3003E73B7 /* MapDetail.swift */, - B52625AF25C87C14003E73B7 /* MapRender.swift */, - B52625C525C8BD2A003E73B7 /* Stage.swift */, ); path = Map; sourceTree = ""; @@ -159,11 +204,35 @@ children = ( B52625BA25C884C2003E73B7 /* Map+parse.swift */, B52625AA25C87909003E73B7 /* Date+format.swift */, - B52625B525C87D69003E73B7 /* Binding+unwrap.swift */, + B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */, ); path = Extensions; sourceTree = ""; }; + B539516A25CB0C7800959F72 /* State */ = { + isa = PBXGroup; + children = ( + B526257A25C874FA003E73B7 /* Persistence.swift */, + B539516B25CB0C9200959F72 /* Store.swift */, + B539517325CB0CA400959F72 /* AppState.swift */, + ); + path = State; + sourceTree = ""; + }; + B539517925CB0D6100959F72 /* Views */ = { + isa = PBXGroup; + children = ( + B526257125C874F9003E73B7 /* MapApp.swift */, + B526257325C874F9003E73B7 /* ContentView.swift */, + B52625A525C876C3003E73B7 /* MapDetail.swift */, + B52625C525C8BD2A003E73B7 /* Stage.swift */, + B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */, + B539518025CB2D7A00959F72 /* MapTextEditor.swift */, + B52625AF25C87C14003E73B7 /* MapRender.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -294,15 +363,26 @@ buildActionMask = 2147483647; files = ( B52625B025C87C14003E73B7 /* MapRender.swift in Sources */, + B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */, B52625AB25C87909003E73B7 /* Date+format.swift in Sources */, - B52625B625C87D69003E73B7 /* Binding+unwrap.swift in Sources */, + B523C77125CA121300C44061 /* MapBlockers.swift in Sources */, + B523C76725CA071B00C44061 /* MapVertices.swift in Sources */, + B539517425CB0CA400959F72 /* AppState.swift in Sources */, + B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */, + B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */, B52625BB25C884C2003E73B7 /* Map+parse.swift in Sources */, B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */, + B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */, B526257B25C874FA003E73B7 /* Persistence.swift in Sources */, + B523C77625CA181100C44061 /* MapColor.swift in Sources */, B526257425C874F9003E73B7 /* ContentView.swift in Sources */, B526257E25C874FA003E73B7 /* Map.xcdatamodeld in Sources */, + B523C74B25C9C1BA00C44061 /* DefaultMapView.swift in Sources */, + B539516C25CB0C9300959F72 /* Store.swift in Sources */, B526257225C874F9003E73B7 /* MapApp.swift in Sources */, B52625A625C876C3003E73B7 /* MapDetail.swift in Sources */, + B523C76225CA05A300C44061 /* MapStages.swift in Sources */, + B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -470,7 +550,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 11; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -494,7 +575,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 11; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/Map/Assets.xcassets/AccentColor.colorset/Contents.json b/Map/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..c981db7 100644 --- a/Map/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Map/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.871", + "red" : "1.000" + } + }, "idiom" : "universal" } ], diff --git a/Map/Assets.xcassets/AppIcon.appiconset/Contents.json b/Map/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..64dc11e 100644 --- a/Map/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Map/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,51 +1,61 @@ { "images" : [ { + "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..b4618d4 Binary files /dev/null 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 new file mode 100644 index 0000000..dfdf396 Binary files /dev/null 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 new file mode 100644 index 0000000..fb22da2 Binary files /dev/null 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 new file mode 100644 index 0000000..a2ea184 Binary files /dev/null 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 new file mode 100644 index 0000000..cbe1cfc Binary files /dev/null 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 new file mode 100644 index 0000000..64bc6b5 Binary files /dev/null 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 new file mode 100644 index 0000000..a2ea184 Binary files /dev/null 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 new file mode 100644 index 0000000..5663b5e Binary files /dev/null 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 new file mode 100644 index 0000000..64bc6b5 Binary files /dev/null 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 new file mode 100644 index 0000000..bfa4365 Binary files /dev/null and b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/Map/ContentView.swift b/Map/ContentView.swift deleted file mode 100644 index ae5b6cd..0000000 --- a/Map/ContentView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], - animation: .default) - private var maps: FetchedResults - - var body: some View { - NavigationView { - List { - ForEach(maps) { map in - NavigationLink(destination: MapDetailView(map: map)) { - HStack { - Text(map.title ?? "Untitled Map") - Text(map.createdAt!.format()) - .font(.footnote) - .foregroundColor(Color.accentColor) - } - } - } - .onDelete(perform: deleteMaps) - } - .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 { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - 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 { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let mapFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Extensions/Binding+unwrap.swift b/Map/Extensions/Binding+unwrap.swift deleted file mode 100644 index 3498cc3..0000000 --- a/Map/Extensions/Binding+unwrap.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI - -extension Binding { - init(_ source: Binding, _ defaultValue: Value) { - // Ensure a non-nil value in `source`. - if source.wrappedValue == nil { - source.wrappedValue = defaultValue - } - // Unsafe unwrap because *we* know it's non-nil now. - self.init(source)! - } -} diff --git a/Map/Extensions/Date+format.swift b/Map/Extensions/Date+format.swift index c12e67d..d29ed0f 100644 --- a/Map/Extensions/Date+format.swift +++ b/Map/Extensions/Date+format.swift @@ -8,10 +8,10 @@ import Foundation extension Date { - func format() -> String { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter.string(from: self) - } + 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 index 03e7993..d82ba99 100644 --- a/Map/Extensions/Map+parse.swift +++ b/Map/Extensions/Map+parse.swift @@ -1,106 +1,215 @@ import CoreGraphics import Foundation -let VERTEX_PATTERN = "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)" -let EDGE_PATTERN = "(.+?)[\\s]*->[\\s]*(.+)" +let vertexPattern = + "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)" +let edgePattern = "(.+?)[\\s]*-([->])[\\s]*(.+)" +let blockerPattern = "\\[Blocker\\][\\s]*(.+)" +let opportunityPattern = "\\[Opportunity\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)" +let stagePattern = "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)" struct ParsedMap { - let vertices: [Vertex] - let edges: [MapEdge] + let vertices: [Vertex] + let edges: [MapEdge] + let blockers: [Blocker] + let opportunities: [Opportunity] + let stages: [CGFloat] } struct Vertex { - let id: Int - let label: String - let position: CGPoint + let id: Int + let label: String + let position: CGPoint } struct MapEdge { - let id: Int - let origin: CGPoint - let destination: CGPoint + 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 +} + +let defaultDimensions: [CGFloat] = [ + 25.0, + 50.0, + 75.0, +] + // Extracts the vertices from the text func parseVertices(_ text: String) -> [String: CGPoint] { - - var result: [String: CGPoint] = [:] - let regex = try! NSRegularExpression(pattern: VERTEX_PATTERN, options: .caseInsensitive) - - let lines = text.split(whereSeparator: \.isNewline) - - for line in lines { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - if matches.count > 0 && matches[0].numberOfRanges == 4{ - - 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) - let point = CGPoint(x: x, y: y) - - result[key] = point - } + + var result: [String: CGPoint] = [:] + let regex = try! NSRegularExpression(pattern: vertexPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for line in lines { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + 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) + let point = CGPoint(x: x, y: y) + + result[key] = point } - - return result -} + } -// Extracts the edges from the text + return result +} func parseEdges(_ text: String, vertices: [String: CGPoint]) -> [MapEdge] { - - var result: [MapEdge] = [] - let regex = try! NSRegularExpression(pattern: EDGE_PATTERN, options: .caseInsensitive) - - let lines = text.split(whereSeparator: \.isNewline) - - for (index, line) in lines.enumerated() { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - if matches.count > 0 && matches[0].numberOfRanges == 3 { - - let match = matches[0]; - let vertexA = String(line[Range(match.range(at: 1), in: line)!]) - let vertexB = String(line[Range(match.range(at: 2), in: line)!]) - - if let origin = vertices[vertexA] { - if let destination = vertices[vertexB] { - result.append(MapEdge(id: index, origin: origin, destination: destination)) - } - } + + var result: [MapEdge] = [] + let regex = try! NSRegularExpression(pattern: edgePattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + 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] { + result.append( + MapEdge(id: index, origin: origin, destination: destination, arrowhead: arrowhead)) } + } } - - return result + } + + return result +} + +func parseOpportunities(_ text: String, vertices: [String: CGPoint]) -> [Opportunity] { + + var result: [Opportunity] = [] + let regex = try! NSRegularExpression(pattern: opportunityPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + let match = matches[0] + let multiplier = CGFloat( + String(line[Range(match.range(at: 2), in: line)!]) == "-" ? -1.0 : 1.0) + let vertex = String(line[Range(match.range(at: 1), in: line)!]) + let opportunityString = String(line[Range(match.range(at: 3), in: line)!]) + let opportunity = CGFloat( + truncating: NumberFormatter().number(from: opportunityString) ?? 0.0) + + if let origin = vertices[vertex] { + let destination = CGPoint(x: origin.x + opportunity * multiplier, y: origin.y) + result.append(Opportunity(id: index, origin: origin, destination: destination)) + } + } + } + + return result +} + +func parseBlockers(_ text: String, vertices: [String: CGPoint]) -> [Blocker] { + + var result: [Blocker] = [] + let regex = try! NSRegularExpression(pattern: blockerPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 2 { + + let match = matches[0] + let vertexA = String(line[Range(match.range(at: 1), in: line)!]) + + if let position = vertices[vertexA] { + result.append(Blocker(id: index, position: position)) + } + } + } + + return result +} + +func parseStages(_ text: String) -> [CGFloat] { + + var result = defaultDimensions + let regex = try! NSRegularExpression(pattern: stagePattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for line in lines { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 3 { + + 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) + + result[stage.count - 1] = dimensions + } + } + + return result } // Converts vetex dictionary to array func mapVertices(_ vertices: [String: CGPoint]) -> [Vertex] { - var i = 0 - return vertices.map { label, position in - i += 1; - return Vertex(id: i, label: label, position: position) - } + var i = 0 + return vertices.map { label, position in + i += 1 + return Vertex(id: i, label: label, position: position) + } } extension Map { - func parse() -> ParsedMap { - - let text = self.content ?? "" - let vertices = parseVertices(text) - let mappedVertices = mapVertices(vertices) - let edges = parseEdges(text, vertices: vertices) - print(mappedVertices) - print(edges) - - return ParsedMap(vertices: mappedVertices, edges: edges) - } + func parse() -> ParsedMap { + + let text = self.content ?? "" + let vertices = parseVertices(text) + let mappedVertices = mapVertices(vertices) + let edges = parseEdges(text, vertices: vertices) + let blockers = parseBlockers(text, vertices: vertices) + let opportunities = parseOpportunities(text, vertices: vertices) + let stages = parseStages(text) + + return ParsedMap( + vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, + stages: stages) + } } diff --git a/Map/Extensions/NSImage+writePNG.swift b/Map/Extensions/NSImage+writePNG.swift new file mode 100644 index 0000000..c24c0cf --- /dev/null +++ b/Map/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/Info.plist b/Map/Info.plist index 69c84ae..ca51786 100644 --- a/Map/Info.plist +++ b/Map/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion 1 LSMinimumSystemVersion diff --git a/Map/Map.entitlements b/Map/Map.entitlements index f2ef3ae..6060301 100644 --- a/Map/Map.entitlements +++ b/Map/Map.entitlements @@ -2,9 +2,19 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.pizza.unlimited.map + + com.apple.developer.icloud-services + + CloudKit + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + diff --git a/Map/MapApp.swift b/Map/MapApp.swift deleted file mode 100644 index 10b6bcb..0000000 --- a/Map/MapApp.swift +++ /dev/null @@ -1,20 +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) - } - } -} diff --git a/Map/MapDetail.swift b/Map/MapDetail.swift deleted file mode 100644 index 74e3c8d..0000000 --- a/Map/MapDetail.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData - -struct MapDetailView: View { - @Environment(\.managedObjectContext) private var viewContext - - @ObservedObject var map: Map - - @State private var selectedEvolution = StageType.General - - var body: some View { - VSplitView { - VStack { - TextField("Title", text: Binding($map.title, ""), onCommit: { - try? viewContext.save() - }) - Picker("Evolution", selection: $selectedEvolution) { - ForEach(StageType.allCases) { stage in - Text(Stage.title(stage)).tag(stage) - } - } - TextEditor(text: Binding($map.content, "")).onChange(of: map.content) { _ in - try? viewContext.save() - }.font(Font.system(size: 16, design: .monospaced)) - } - ScrollView([.horizontal, .vertical]) { - MapRenderView(map: map, evolution: Stage.stages(selectedEvolution)) - } - } - } -} - -struct MapDetailView_Previews: PreviewProvider { - static var previews: some View { - MapDetailView(map: Map()).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/MapRender.swift b/Map/MapRender.swift deleted file mode 100644 index 79d80b5..0000000 --- a/Map/MapRender.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData -import CoreGraphics - -struct MapRenderView: View { - @ObservedObject var map: Map - let evolution: Stage - - let VERTEX_SIZE = CGSize(width: 25.0, height: 25.0) - let ARROWHEAD_SIZE = CGFloat(10.0) - let LINE_WIDTH = CGFloat(1.0) - let PADDING = CGFloat(4.0) - let STAGE_FRAME = CGSize(width: 245, height: 50) - - var parsedMap: ParsedMap { - return map.parse() - } - - var body: some View { - ZStack(alignment: .topLeading) { - ZStack (alignment: .topLeading) { - // The Axes - Path { path in - path.move(to: CGPoint(x: 0, y: 0)) - path.addLine(to: CGPoint(x: 0, y: 1000)) - path.addLine(to: CGPoint(x: 1000, y: 1000)) - path.move(to: CGPoint(x: 1000, y: 1000)) - path.closeSubpath() - - }.strokedPath(StrokeStyle(lineWidth: LINE_WIDTH * 2)) - Text("Visible").font(.title3).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -35.0, height: 0.0)) - Text("Value Chain").font(.title).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -72.0, height: 480.0)) - Text("Invisible").font(.title3).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -40.0, height: 980.0)) - Text(evolution.I).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 0.0, height: 1000.0)) - Text(evolution.II).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 250.0, height: 1000.0)) - Text(evolution.III).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 500.0, height: 1000.0)) - Text(evolution.IV).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 750.0, height: 1000.0)) - }.offset(CGSize(width: 0.0, height: 0.0)) - - // The Lanes - Path { path in - path.move(to: CGPoint(x: 250, y: 0)) - path.addLine(to: CGPoint(x: 250, y: 1000)) - path.move(to: CGPoint(x: 500, y: 0)) - path.addLine(to: CGPoint(x: 500, y: 1000)) - path.move(to: CGPoint(x: 750, y: 0)) - path.addLine(to: CGPoint(x: 750, y: 1000)) - path.move(to: CGPoint(x: 250, y: 0)) - path.closeSubpath() - }.strokedPath(StrokeStyle(lineWidth: LINE_WIDTH, dash: [10.0])) - - Path { path in - path.addRect(CGRect(x: 0, y: 0, width: 250, height: 1000)) - }.fill(Color.red).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 250, y: 0, width: 250, height: 1000)) - }.fill(Color.orange).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 500, y: 0, width: 250, height: 1000)) - }.fill(Color.yellow).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 750, y: 0, width: 250, height: 1000)) - }.fill(Color.green).opacity(0.1) - - // The Vertices - - ForEach(parsedMap.vertices, id: \.id) { vertex in - Path { path in - path.addEllipse(in: CGRect( - origin: vertex.position, size: VERTEX_SIZE - )) - } - Text(vertex.label).foregroundColor(Color.gray).offset(CGSize( - width: vertex.position.x + VERTEX_SIZE.width + PADDING, - height: vertex.position.y)) - } - - // The Edges - - ForEach(parsedMap.edges, id: \.id) { edge in - Path { path in - - let slope = (edge.destination.y - edge.origin.y) / (edge.destination.x - edge.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: edge.origin.x + multiplier * (VERTEX_SIZE.width / 2.0) * cos(angle), y: edge.origin.y + multiplier * (VERTEX_SIZE.height / 2.0) * sin(angle)) - let offsetDestination = CGPoint(x: edge.destination.x - multiplier * (VERTEX_SIZE.width / 2.0) * cos(angle), y: edge.destination.y - multiplier * (VERTEX_SIZE.height / 2.0) * sin(angle)) - - path.move(to: offsetOrigin) - path.addLine(to: offsetDestination) - - // Arrowheads - path.move(to: offsetDestination) - path.addLine(to: CGPoint(x: offsetDestination.x - multiplier * ARROWHEAD_SIZE * cos(upperAngle), y: - offsetDestination.y - multiplier * ARROWHEAD_SIZE * sin(upperAngle))) - - path.move(to: offsetDestination) - path.addLine(to: CGPoint(x: offsetDestination.x - multiplier * ARROWHEAD_SIZE * cos(lowerAngle), y: - offsetDestination.y - multiplier * ARROWHEAD_SIZE * sin(lowerAngle))) - path.move(to: offsetDestination) - path.closeSubpath() - }.applying(CGAffineTransform(translationX: VERTEX_SIZE.width / 2.0, y: VERTEX_SIZE.height / 2.0)).strokedPath(StrokeStyle(lineWidth: LINE_WIDTH)) - } - }.frame( - minWidth: 1050.0, maxWidth: .infinity, - minHeight: 1050.0, maxHeight: .infinity, alignment: .topLeading - ).padding(30.0) - } -} - -struct MapRenderView_Previews: PreviewProvider { - static var previews: some View { - MapDetailView(map: Map()).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/MapRenderComponents/MapAxes.swift b/Map/MapRenderComponents/MapAxes.swift new file mode 100644 index 0000000..e7d65f4 --- /dev/null +++ b/Map/MapRenderComponents/MapAxes.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct MapAxes: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let lineWidth: CGFloat + let evolution: Stage + let stages: [CGFloat] + let stageHeight = CGFloat(50.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, alignment: .topLeading) + .offset(CGSize(width: 0.0, height: -stageHeight / 2.0)) + Text("Industrialised") + .font(.title3) + .foregroundColor(color) + .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 2.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 new file mode 100644 index 0000000..b668b30 --- /dev/null +++ b/Map/MapRenderComponents/MapBlockers.swift @@ -0,0 +1,49 @@ +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/MapColor.swift b/Map/MapRenderComponents/MapColor.swift new file mode 100644 index 0000000..ce3d453 --- /dev/null +++ b/Map/MapRenderComponents/MapColor.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct MapColor { + let foreground: Color + let background: Color + let secondary: Color + let blocker: Color + let opportunity: Color + let stages: StageColor + + 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))) + } 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))) + } + } +} + +struct StageColor { + let i: Color + let ii: Color + let iii: Color + let iv: Color +} diff --git a/Map/MapRenderComponents/MapEdges.swift b/Map/MapRenderComponents/MapEdges.swift new file mode 100644 index 0000000..d40d2aa --- /dev/null +++ b/Map/MapRenderComponents/MapEdges.swift @@ -0,0 +1,92 @@ +// +// 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 new file mode 100644 index 0000000..68d3fb5 --- /dev/null +++ b/Map/MapRenderComponents/MapOpportunities.swift @@ -0,0 +1,84 @@ +// +// 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 new file mode 100644 index 0000000..052a315 --- /dev/null +++ b/Map/MapRenderComponents/MapStages.swift @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000..71f85be --- /dev/null +++ b/Map/MapRenderComponents/MapVertices.swift @@ -0,0 +1,49 @@ +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 + Path { path in + path.addEllipse( + in: CGRect( + origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize + )) + }.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)) + } +} + +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/Persistence.swift b/Map/Persistence.swift deleted file mode 100644 index 49bb2d9..0000000 --- a/Map/Persistence.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Persistence.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -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 { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Map") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/Map/Stage.swift b/Map/Stage.swift deleted file mode 100644 index 4a53ada..0000000 --- a/Map/Stage.swift +++ /dev/null @@ -1,108 +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 Built", III: "Product (+rental)", IV: "Commodity (+utility)") - 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: "Needs to be pushed", II: "Shown in simple situations", III: "Shown in moderately complex situations ", IV: "Always shown") - } - } - - static func title(_ type: StageType) -> String { - switch(type) { - case .General: - return "General" - 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 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 } -} - - diff --git a/Map/State/AppState.swift b/Map/State/AppState.swift new file mode 100644 index 0000000..e10efea --- /dev/null +++ b/Map/State/AppState.swift @@ -0,0 +1,103 @@ +import Cocoa +import Foundation +import SwiftUI + +struct AppState { + var selectedMap: Map? = nil + var mapBeingDeleted: Map? = nil +} + +enum AppAction { + case selectMap(map: Map?) + case deleteMap(map: Map) + case exportMapAsImage(map: Map, evolution: StageType) + case exportMapAsText(map: Map) +} + +func appStateReducer(state: inout AppState, action: AppAction) { + + switch action { + + case .selectMap(let map): + state.selectedMap = map + + case .deleteMap(let map): + let context = PersistenceController.shared.container.viewContext + + context.delete(map) + try? context.save() + + case .exportMapAsImage(let map, let evolution): + 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(map: map, evolution: Stage.stages(evolution)) + + 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") + } + } + } +} + +typealias AppStore = Store diff --git a/Map/State/Persistence.swift b/Map/State/Persistence.swift new file mode 100644 index 0000000..1eb09e8 --- /dev/null +++ b/Map/State/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/State/Store.swift b/Map/State/Store.swift new file mode 100644 index 0000000..7860f33 --- /dev/null +++ b/Map/State/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/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 + + 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 { + Binding( + get: { map.title ?? "" }, + set: { title in + map.title = title + } + ) + } + + private var content: Binding { + 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) { + 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 + ) -> MapTextEditorController { + return MapTextEditorController(text: $text) + } + + func updateNSViewController( + _ nsViewController: MapTextEditorController, + context: NSViewControllerRepresentableContext + ) { + } +} 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 } +} diff --git a/MapTests/MapTests.swift b/MapTests/MapTests.swift index 4f2a183..e425c00 100644 --- a/MapTests/MapTests.swift +++ b/MapTests/MapTests.swift @@ -6,28 +6,29 @@ // import XCTest + @testable import Map class MapTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. } + } } diff --git a/MapUITests/MapUITests.swift b/MapUITests/MapUITests.swift index ee1eeca..d2d523d 100644 --- a/MapUITests/MapUITests.swift +++ b/MapUITests/MapUITests.swift @@ -9,34 +9,34 @@ import XCTest class MapUITests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } } + } } diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c8daf1 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Map + +A wardley mapping tool: Write some text, get a diagram + +## Building + +Open the project in Xcode and press buithe run button + +## Formatting and Linting + +In order to format and lint the code, the project provides a Makefile that +uses [swift-format][swift-format]. + +* To format: `make format` +* To lint: `make lint` + +These commands run on every `.swift` file in the directory. + +## Language Reference + +### Nodes + +Nodes should be of the format `Name (x,y)`. The name can contain spaces, and +the `x`/`y` can be integers or decimals. All dimensions go from 0 - 100, so +50 means 50% of the way through. eg. + +- `Node (1,2)` +- `My Cool Node (1.0,2.0)` +- `A (1, 2.0)` + +### Edges + +Edges connect two nodes. They use the format `Node -- Node` (line only) or +`Node -> Node` (with arrowhead). eg. + +- `Node -- My Cool Node` +- `A -> Node` + +### Blockers + +You can place a blocker in front of a node by using `[Blocker] Node`. eg. + +- `[Blocker] My Cool Node` +- `[Blocker] A` + +### Opportunities + +You can draw opportunity arrows by using `[Opportunity] Node +x` or +`[Opportunity] Node -x`. eg. + +- `[Opportunity] My Cool Node -10` +- `[Opportunity] A +15` + +### Modifying the axes + +If you need more space for one of the four segments you can use +`[I] x`, `[II] x`, or `[III] x`. eg. + +- `[I] 15` +- `[II] 35.5` +- `[III] 80` + +The parser doesn't enforce position, so if you put axis iii before axis i, +you'll get some rendering issues. + +[swift-format]: https://github.com/apple/swift-format.git