--- /dev/null
+# 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
--- /dev/null
+ 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.
--- /dev/null
+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
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 */; };
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 */
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
+ B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+writePNG.swift"; sourceTree = "<group>"; };
+ 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 = "<group>"; };
+ B523C75925C9FD4900C44061 /* MapAxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAxes.swift; sourceTree = "<group>"; };
+ B523C76125CA05A300C44061 /* MapStages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapStages.swift; sourceTree = "<group>"; };
+ B523C76625CA071B00C44061 /* MapVertices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVertices.swift; sourceTree = "<group>"; };
+ B523C76B25CA0DFA00C44061 /* MapEdges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEdges.swift; sourceTree = "<group>"; };
+ B523C77025CA121300C44061 /* MapBlockers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapBlockers.swift; sourceTree = "<group>"; };
+ B523C77525CA181100C44061 /* MapColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapColor.swift; sourceTree = "<group>"; };
+ B523C77D25CA294C00C44061 /* MapOpportunities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapOpportunities.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B526257325C874F9003E73B7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
B52625A525C876C3003E73B7 /* MapDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDetail.swift; sourceTree = "<group>"; };
B52625AA25C87909003E73B7 /* Date+format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+format.swift"; sourceTree = "<group>"; };
B52625AF25C87C14003E73B7 /* MapRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRender.swift; sourceTree = "<group>"; };
- B52625B525C87D69003E73B7 /* Binding+unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+unwrap.swift"; sourceTree = "<group>"; };
B52625BA25C884C2003E73B7 /* Map+parse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Map+parse.swift"; sourceTree = "<group>"; };
B52625C525C8BD2A003E73B7 /* Stage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = "<group>"; };
+ B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
+ B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
+ B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ B523C74625C9BD3500C44061 /* CloudKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ B523C74425C9BD3500C44061 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ B523C74525C9BD3500C44061 /* CloudKit.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 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 = "<group>";
+ };
B526256525C874F9003E73B7 = {
isa = PBXGroup;
children = (
B526258825C874FA003E73B7 /* MapTests */,
B526259325C874FA003E73B7 /* MapUITests */,
B526256F25C874F9003E73B7 /* Products */,
+ B523C74425C9BD3500C44061 /* Frameworks */,
);
sourceTree = "<group>";
};
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 = "<group>";
children = (
B52625BA25C884C2003E73B7 /* Map+parse.swift */,
B52625AA25C87909003E73B7 /* Date+format.swift */,
- B52625B525C87D69003E73B7 /* Binding+unwrap.swift */,
+ B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
+ B539516A25CB0C7800959F72 /* State */ = {
+ isa = PBXGroup;
+ children = (
+ B526257A25C874FA003E73B7 /* Persistence.swift */,
+ B539516B25CB0C9200959F72 /* Store.swift */,
+ B539517325CB0CA400959F72 /* AppState.swift */,
+ );
+ path = State;
+ sourceTree = "<group>";
+ };
+ 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 = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
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;
};
"$(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;
"$(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;
{
"colors" : [
{
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.969",
+ "green" : "0.871",
+ "red" : "1.000"
+ }
+ },
"idiom" : "universal"
}
],
{
"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"
+++ /dev/null
-//
-// 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<Map>
-
- 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)
- }
-}
+++ /dev/null
-import SwiftUI
-
-extension Binding {
- init(_ source: Binding<Value?>, _ 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)!
- }
-}
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)
+ }
}
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)
+ }
}
--- /dev/null
+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)")
+ }
+ }
+}
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
- <string>1.0</string>
+ <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
- <key>com.apple.security.app-sandbox</key>
- <true/>
- <key>com.apple.security.files.user-selected.read-only</key>
- <true/>
+ <key>com.apple.developer.aps-environment</key>
+ <string>development</string>
+ <key>com.apple.developer.icloud-container-identifiers</key>
+ <array>
+ <string>iCloud.pizza.unlimited.map</string>
+ </array>
+ <key>com.apple.developer.icloud-services</key>
+ <array>
+ <string>CloudKit</string>
+ </array>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+ <key>com.apple.security.files.user-selected.read-write</key>
+ <true/>
</dict>
</plist>
+++ /dev/null
-//
-// 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)
- }
- }
-}
+++ /dev/null
-//
-// 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)
- }
-}
+++ /dev/null
-//
-// 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)
- }
-}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)),
+ ])
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+//
+// 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)
+ ])
+ }
+}
--- /dev/null
+//
+// 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))
+ ])
+ }
+}
--- /dev/null
+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])
+ }
+}
--- /dev/null
+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)),
+ ])
+ }
+}
+++ /dev/null
-//
-// 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)")
- }
- })
- }
-}
+++ /dev/null
-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 }
-}
-
-
--- /dev/null
+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<AppState, AppAction>
--- /dev/null
+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)")
+ }
+ })
+ }
+}
--- /dev/null
+import Foundation
+
+final class Store<State, Action>: ObservableObject {
+ @Published private(set) var state: State
+
+ private let reducer: Reducer<State, Action>
+
+ init(initialState: State, reducer: @escaping Reducer<State, Action>) {
+ self.state = initialState
+ self.reducer = reducer
+ }
+
+ func send(_ action: Action) {
+ reducer(&state, action)
+ }
+}
+
+typealias Reducer<State, Action> = (inout State, Action) -> Void
--- /dev/null
+//
+// ContentView.swift
+// Map
+//
+// Created by Ruben Beltran del Rio on 2/1/21.
+//
+
+import CoreData
+import SwiftUI
+
+struct ContentView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+
+ @FetchRequest(
+ sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)],
+ animation: .default)
+ private var maps: FetchedResults<Map>
+
+ var body: some View {
+ NavigationView {
+ List {
+ if maps.count == 0 {
+ DefaultMapView()
+ }
+ ForEach(maps) { map in
+ NavigationLink(destination: MapDetailView(map: map)) {
+ HStack {
+ Text(map.title ?? "Untitled Map")
+ Spacer()
+ Text(mapFormatter.string(from: (map.createdAt ?? Date())))
+ .font(.caption)
+ .padding(.vertical, 2.0)
+ .padding(.horizontal, 4.0)
+ .background(Color.accentColor)
+ .foregroundColor(Color.black)
+ .cornerRadius(2.0)
+ }.padding(.leading, 8.0)
+ }
+ }
+ .onDelete(perform: deleteMaps)
+ }.frame(minWidth: 250.0, alignment: .leading)
+ .toolbar {
+ HStack {
+ Button(action: toggleSidebar) {
+ Label("Toggle Sidebar", systemImage: "sidebar.left")
+ }
+ Button(action: addMap) {
+ Label("Add Map", systemImage: "plus")
+ }
+ }
+ }
+ }
+ }
+
+ private func toggleSidebar() {
+ NSApp.keyWindow?.firstResponder?.tryToPerform(
+ #selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
+ }
+
+ private func addMap() {
+ withAnimation {
+ let newMap = Map(context: viewContext)
+ newMap.uuid = UUID()
+ newMap.createdAt = Date()
+ newMap.title = "Map \(newMap.createdAt!.format())"
+ newMap.content = ""
+
+ do {
+ try viewContext.save()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+ }
+
+ private func deleteMaps(offsets: IndexSet) {
+
+ withAnimation {
+ offsets.map { maps[$0] }.forEach(viewContext.delete)
+
+ do {
+ try viewContext.save()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+ }
+}
+
+private let mapFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .none
+ return formatter
+}()
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView().environment(
+ \.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+}
--- /dev/null
+//
+// 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)
+ }
+}
--- /dev/null
+//
+// 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())
+ }
+}
--- /dev/null
+//
+// ContentView.swift
+// Map
+//
+// Created by Ruben Beltran del Rio on 2/1/21.
+//
+
+import CoreData
+import SwiftUI
+
+struct MapDetailView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @Environment(\.colorScheme) var colorScheme
+
+ @EnvironmentObject var store: AppStore
+
+ @ObservedObject var map: Map
+
+ private var mapColor: MapColor {
+ MapColor.colorForScheme(colorScheme)
+ }
+
+ private var title: Binding<String> {
+ Binding(
+ get: { map.title ?? "" },
+ set: { title in
+ map.title = title
+ }
+ )
+ }
+
+ private var content: Binding<String> {
+ Binding(
+ get: { map.content ?? "" },
+ set: { content in
+ map.content = content
+ }
+ )
+ }
+
+ @State private var selectedEvolution = StageType.general
+
+ var body: some View {
+ if map.uuid != nil {
+ VSplitView {
+ VStack {
+ HStack {
+ TextField(
+ "Title", text: title,
+ onCommit: {
+ try? viewContext.save()
+ }
+ ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding(
+ .leading, 4.0)
+ Button(action: saveText) {
+ Image(systemName: "doc.text")
+ }.padding(.vertical, 4.0).padding(.leading, 4.0)
+ Button(action: saveImage) {
+ Image(systemName: "photo")
+ }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
+ }
+ Picker("Evolution", selection: $selectedEvolution) {
+ ForEach(StageType.allCases) { stage in
+ Text(Stage.title(stage)).tag(stage).padding(4.0)
+ }
+ }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+
+ ZStack(alignment: .topLeading) {
+ MapTextEditor(text: content).onChange(of: map.content) { _ in
+ try? viewContext.save()
+ }
+ .background(mapColor.background)
+ .foregroundColor(mapColor.foreground)
+ .frame(minHeight: 96.0)
+ }.padding(.top, 8.0).padding(.leading, 8.0).background(mapColor.background).cornerRadius(
+ 5.0)
+ }.padding(.horizontal, 8.0)
+ ScrollView([.horizontal, .vertical]) {
+ MapRenderView(map: map, evolution: Stage.stages(selectedEvolution))
+ }.background(mapColor.background)
+ }
+ } else {
+ DefaultMapView()
+ }
+ }
+
+ private func saveText() {
+ store.send(.exportMapAsText(map: map))
+ }
+
+ private func saveImage() {
+ store.send(.exportMapAsImage(map: map, evolution: selectedEvolution))
+ }
+}
+
+struct MapDetailView_Previews: PreviewProvider {
+ static var previews: some View {
+ MapDetailView(map: Map()).environment(
+ \.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+}
--- /dev/null
+//
+// 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)
+ }
+}
--- /dev/null
+import Cocoa
+import SwiftUI
+
+class MapTextEditorController: NSViewController {
+
+ @Binding var text: String
+
+ init(text: Binding<String>) {
+ self._text = text
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ let scrollView = NSTextView.scrollableTextView()
+ let textView = scrollView.documentView as! NSTextView
+
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+
+ textView.delegate = self
+ textView.string = self.text
+ textView.isEditable = true
+ textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
+ self.view = scrollView
+ }
+
+ override func viewDidAppear() {
+ self.view.window?.makeFirstResponder(self.view)
+ }
+}
+
+extension MapTextEditorController: NSTextViewDelegate {
+
+ func textDidChange(_ obj: Notification) {
+ if let textField = obj.object as? NSTextView {
+ self.text = textField.string
+ }
+ }
+
+ func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool
+ {
+ let range = Range(shouldChangeTextIn, in: view.string)
+ let target = view.string[range!]
+
+ if target == "--" {
+ return false
+ }
+
+ return true
+ }
+}
+
+struct MapTextEditor: NSViewControllerRepresentable {
+
+ @Binding var text: String
+
+ func makeNSViewController(
+ context: NSViewControllerRepresentableContext<MapTextEditor>
+ ) -> MapTextEditorController {
+ return MapTextEditorController(text: $text)
+ }
+
+ func updateNSViewController(
+ _ nsViewController: MapTextEditorController,
+ context: NSViewControllerRepresentableContext<MapTextEditor>
+ ) {
+ }
+}
--- /dev/null
+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 }
+}
//
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.
}
+ }
}
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()
+ }
}
+ }
}
--- /dev/null
+# 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