diff --git a/packages/column-builder/package.json b/packages/column-builder/package.json
index 19e982e17..bb7957d84 100644
--- a/packages/column-builder/package.json
+++ b/packages/column-builder/package.json
@@ -18,12 +18,12 @@
"@macrostrat/form-components": "^0.2.2",
"@macrostrat/hyper": "^3.0.6",
"@macrostrat/ui-components": "^4.1.2",
- "@mapbox/mapbox-gl-draw": "^1.3.0",
+ "@mapbox/mapbox-gl-draw": "^1.5.0",
"@supabase/postgrest-js": "^1.18.1",
"@types/mapbox__mapbox-gl-draw": "^1.2.3",
"axios": "^1.7.9",
"cross-fetch": "^3.1.5",
- "mapbox-gl": "^3.0.0",
+ "mapbox-gl": "^3.13.0",
"react": "^18",
"react-beautiful-dnd": "^13.1.0",
"react-color": "^2.19.3"
diff --git a/packages/column-footprint-editor/.dockerignore b/packages/column-footprint-editor/.dockerignore
new file mode 100644
index 000000000..91b9c5556
--- /dev/null
+++ b/packages/column-footprint-editor/.dockerignore
@@ -0,0 +1,6 @@
+.git
+Dockerfile
+package-lock.json
+/node_modules/*
+/dist/*
+/.parcel-cache/*
\ No newline at end of file
diff --git a/packages/column-footprint-editor/.env.example b/packages/column-footprint-editor/.env.example
new file mode 100644
index 000000000..4da50be1a
--- /dev/null
+++ b/packages/column-footprint-editor/.env.example
@@ -0,0 +1,5 @@
+## .env file must go in the frontend directory
+
+API_BASE=http://geologic_map_api/
+MAPBOX_TOKEN=mapbox_access_token
+## https://docs.mapbox.com/help/getting-started/access-tokens/
diff --git a/packages/column-footprint-editor/.gitignore b/packages/column-footprint-editor/.gitignore
new file mode 100644
index 000000000..28277d9da
--- /dev/null
+++ b/packages/column-footprint-editor/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+.parcel-cache
+Dockerfile.dev
+dist
+__pychache__
\ No newline at end of file
diff --git a/packages/column-footprint-editor/.parcelrc b/packages/column-footprint-editor/.parcelrc
new file mode 100644
index 000000000..bf814fdf8
--- /dev/null
+++ b/packages/column-footprint-editor/.parcelrc
@@ -0,0 +1,8 @@
+{
+ "extends": "@parcel/config-default",
+ "transformers": {
+ "jsx:*.svg": [
+ "@parcel/transformer-svg-react"
+ ]
+ }
+}
diff --git a/packages/column-footprint-editor/Dockerfile b/packages/column-footprint-editor/Dockerfile
new file mode 100644
index 000000000..2fabe3a91
--- /dev/null
+++ b/packages/column-footprint-editor/Dockerfile
@@ -0,0 +1,16 @@
+FROM node:14
+
+WORKDIR /app
+ENV PATH /app/node_modules/.bin:$PATH
+
+
+COPY package*.json ./
+
+RUN npm install
+
+COPY . .
+
+RUN npm run build
+WORKDIR /app/dist
+
+ENTRYPOINT ["npx", "serve", "-p", "1235"]
\ No newline at end of file
diff --git a/packages/column-footprint-editor/package.json b/packages/column-footprint-editor/package.json
new file mode 100644
index 000000000..08cf747f3
--- /dev/null
+++ b/packages/column-footprint-editor/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@macrostrat/column-footprint-editor",
+ "private": true,
+ "main": "src/index.tsx",
+ "scripts": {
+ "start": "ENV=development parcel serve -p 1235 ./src/index.html",
+ "build": "ENV=production parcel build --public-url /column-topology/ ./src/index.html"
+ },
+ "dependencies": {
+ "@blueprintjs/core": "^5.10.2",
+ "@blueprintjs/popover2": "^1.4.1",
+ "@blueprintjs/select": "^5.1.4",
+ "@macrostrat/ui-components": "^4.4.4",
+ "@mapbox/mapbox-gl-draw": "^1.5.0",
+ "@turf/nearest-point-on-line": "^5.1.5",
+ "@types/mapbox__mapbox-gl-draw": "^1.2.4",
+ "axios": "^1.11.0",
+ "mapbox-gl": "^3.13.0",
+ "mapbox-gl-draw-snap-mode": "^0.4.0",
+ "react": "^18.3.0",
+ "react-color": "^2.19.3",
+ "react-dom": "^18.3.0",
+ "react-loading-overlay": "^1.0.1",
+ "react-router-dom": "^5.3.0",
+ "topojson-client": "^3.1.0"
+ }
+}
diff --git a/packages/column-footprint-editor/src/assets/12-side.svg b/packages/column-footprint-editor/src/assets/12-side.svg
new file mode 100644
index 000000000..f0227a61c
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/12-side.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/assets/16-side.svg b/packages/column-footprint-editor/src/assets/16-side.svg
new file mode 100644
index 000000000..93c7248b2
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/16-side.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/assets/20-side.svg b/packages/column-footprint-editor/src/assets/20-side.svg
new file mode 100644
index 000000000..9e878ed74
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/20-side.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/assets/24-side.svg b/packages/column-footprint-editor/src/assets/24-side.svg
new file mode 100644
index 000000000..3066d844b
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/24-side.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/assets/4-side.svg b/packages/column-footprint-editor/src/assets/4-side.svg
new file mode 100644
index 000000000..a21bf90d2
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/4-side.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/assets/8-side.svg b/packages/column-footprint-editor/src/assets/8-side.svg
new file mode 100644
index 000000000..e878863a4
--- /dev/null
+++ b/packages/column-footprint-editor/src/assets/8-side.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/packages/column-footprint-editor/src/components/blueprint/buttons.tsx b/packages/column-footprint-editor/src/components/blueprint/buttons.tsx
new file mode 100644
index 000000000..d5c9cfb06
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/buttons.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { Button } from "@blueprintjs/core";
+import { useAPIResult } from "@macrostrat/ui-components";
+import { base } from "../../context/env";
+
+function SaveButton(props) {
+ const { onClick, minimal, disabled } = props;
+
+ return (
+
+ );
+}
+
+function downloadObjectAsJson(exportObj, exportName) {
+ var dataStr = "data:text/csv;charset=utf-8," + encodeURIComponent(exportObj);
+ var downloadAnchorNode = document.createElement("a");
+ downloadAnchorNode.setAttribute("href", dataStr);
+ downloadAnchorNode.setAttribute("download", exportName + ".csv");
+ document.body.appendChild(downloadAnchorNode); // required for firefox
+ downloadAnchorNode.click();
+ downloadAnchorNode.remove();
+}
+
+function DownloadButton(props) {
+ const { project_id } = props;
+ let data = [];
+ if (project_id) {
+ data = useAPIResult(base + `${project_id}/csv`);
+ }
+
+ const onClick = () => {
+ downloadObjectAsJson(data, "columns");
+ };
+ return (
+
+ );
+}
+
+export { SaveButton, DownloadButton };
diff --git a/packages/column-footprint-editor/src/components/blueprint/dialog.tsx b/packages/column-footprint-editor/src/components/blueprint/dialog.tsx
new file mode 100644
index 000000000..1fb327f07
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/dialog.tsx
@@ -0,0 +1,89 @@
+import React, { useState, useEffect } from "react";
+import { Overlay, Button, Card } from "@blueprintjs/core";
+import "./main.css";
+
+function OverlayBox(props) {
+ const {
+ open,
+ children,
+ closeOpen,
+ HeaderComponent,
+ className = "overlay",
+ closeButton = true,
+ cardStyles = {},
+ } = props;
+
+ const [state, setState] = useState({ top: 100, left: 20 });
+ const [offset, setOffset] = useState({ rel_x: 0, rel_y: 0 });
+ const [dragging, setDragging] = useState(false);
+
+ useEffect(() => {
+ //setup event listeners
+ if (!dragging) return;
+ document.addEventListener("mousemove", onMouseMove);
+ document.addEventListener("mouseup", onMouseUp);
+ return () => {
+ document.removeEventListener("mouseup", onMouseUp);
+ document.removeEventListener("mousemove", onMouseMove);
+ };
+ }, [dragging]);
+
+ const onMouseDown = (e) => {
+ if (e.button !== 0) return;
+ setDragging(true);
+ setOffset(getOffest(e));
+ };
+
+ const onMouseUp = (e) => {
+ setDragging(false);
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ const getOffest = (e) => {
+ const rel_x = e.pageX - state.left;
+ const rel_y = e.pageY - state.top;
+ return { rel_x, rel_y };
+ };
+
+ const onMouseMove = (e) => {
+ if (dragging) {
+ const { rel_x, rel_y } = offset;
+ const left_ = e.pageX - rel_x;
+ const top_ = e.pageY - rel_y;
+ setState({ top: top_, left: left_ });
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ const overlayProperties = {
+ autoFocus: true,
+ canEscapeKeyClose: true,
+ canOutsideClickClose: true,
+ enforceFocus: false,
+ hasBackdrop: false,
+ usePortal: true,
+ useTallContent: false,
+ };
+
+ const style = { top: `${state.top}px`, left: `${state.left}px` };
+
+ return (
+
+
+
+
+ {children}
+ {closeButton ? (
+
+ ) : null}
+
+
+
+ );
+}
+
+export { OverlayBox };
diff --git a/packages/column-footprint-editor/src/components/blueprint/index.ts b/packages/column-footprint-editor/src/components/blueprint/index.ts
new file mode 100644
index 000000000..a366ad0c9
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/index.ts
@@ -0,0 +1,5 @@
+export * from "./dialog";
+export * from "./navbar";
+export * from "./buttons";
+export * from "./toaster";
+export * from "./select";
diff --git a/packages/column-footprint-editor/src/components/blueprint/main.css b/packages/column-footprint-editor/src/components/blueprint/main.css
new file mode 100644
index 000000000..66d350289
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/main.css
@@ -0,0 +1,62 @@
+
+.overlay {
+ max-width: 500px;
+}
+
+.drag-bar-top {
+ height: 50px;
+ width: 100%;
+ z-index: 22;
+}
+
+.projects-drop-down {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ padding: 5px;
+ justify-content: flex-end;
+}
+
+.projects-drop-more {
+ display: flex;
+ flex-direction: column;
+ padding: 5px;
+ justify-content: flex-end;
+}
+
+.nav-contents {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 10px;
+}
+
+.toolbar {
+ max-width: 300px;
+ margin-top: 5px;
+}
+
+.nav-right,
+.nav-left {
+ display: flex;
+ align-items: center;
+}
+
+.panel-stack {
+ min-height: 500px;
+}
+
+.main-dialog-header {
+ margin-top: 5px;
+ margin-left: 5px;
+}
+
+.btn-holder {
+ float: right;
+ margin-top: -23px;
+ margin-right: -20px;
+}
+
+.project-btns {
+ margin: 0px 5px 10px 0px;
+}
diff --git a/packages/column-footprint-editor/src/components/blueprint/navbar.tsx b/packages/column-footprint-editor/src/components/blueprint/navbar.tsx
new file mode 100644
index 000000000..dbc5f1f64
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/navbar.tsx
@@ -0,0 +1,207 @@
+import React, { useContext } from "react";
+import { DownloadButton } from ".";
+import {
+ Button,
+ Navbar,
+ Popover,
+ MenuItem,
+ Menu,
+ MenuDivider,
+ Icon,
+ IconName,
+} from "@blueprintjs/core";
+import { AppContext } from "../../context";
+import { useAPIResult } from "@macrostrat/ui-components";
+import { base, MAP_MODES } from "../../context";
+
+const projects_url = process.env.API_BASE + "projects";
+
+function unwrapProjects(res) {
+ if (res.data) {
+ const { data } = res;
+ let projects = data.map((project) => {
+ return {
+ project_id: project.project_id,
+ name: project.name,
+ description: project.description,
+ };
+ });
+ return projects;
+ } else {
+ return [];
+ }
+}
+
+function ProjectDropDown(props) {
+ const { projects } = props;
+ const { state, runAction } = useContext(AppContext);
+ const changeProjectId = (project) => {
+ runAction({ type: "change-project", payload: project });
+ };
+
+ const openImportOverlay = () => {
+ runAction({ type: "import-overlay", payload: { open: true } });
+ };
+
+ return (
+
+ );
+}
+
+function NavBarModeBtns(props: {
+ changeMode: (mode: MAP_MODES) => void;
+ mode: MAP_MODES;
+}) {
+ const { changeMode, mode } = props;
+
+ return (
+
+
+
+
+
+ );
+}
+
+interface NavBarSaveBtnsProps {
+ onSave: () => void;
+ onCancel: () => void;
+ mode: MAP_MODES;
+ project_id: number | null;
+ polygons?: object[];
+ changeSet?: object[];
+}
+
+function NavBarSaveBtns(props: NavBarSaveBtnsProps) {
+ const {
+ onSave,
+ onCancel,
+ project_id,
+ mode,
+ polygons = [],
+ changeSet = [],
+ } = props;
+
+ const disabled =
+ (mode == MAP_MODES.voronoi && !polygons.length) ||
+ (mode == MAP_MODES.topology && !changeSet.length);
+ return (
+
+
+
+
+
+ );
+}
+
+interface MapNavBarProps {
+ onSave: () => void;
+ onCancel: () => void;
+ changeMode: (mode: MAP_MODES) => void;
+ mode: MAP_MODES;
+ project_id: number | null;
+ polygons: object[];
+ changeSet: object[];
+}
+
+function MapNavBar(props: MapNavBarProps) {
+ const { onSave, onCancel, mode, changeMode, project_id, ...rest } = props;
+ const { state, runAction } = useContext(AppContext);
+
+ const openImportOverlay = () => {
+ runAction({ type: "import-overlay", payload: { open: true } });
+ };
+
+ const projects = useAPIResult(
+ projects_url,
+ {},
+ { unwrapResponse: unwrapProjects }
+ );
+ if (!projects) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ position="bottom"
+ minimal={true}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export { MapNavBar };
diff --git a/packages/column-footprint-editor/src/components/blueprint/select.tsx b/packages/column-footprint-editor/src/components/blueprint/select.tsx
new file mode 100644
index 000000000..d81eeb81c
--- /dev/null
+++ b/packages/column-footprint-editor/src/components/blueprint/select.tsx
@@ -0,0 +1,92 @@
+import { Suggest } from "@blueprintjs/select";
+import { Icon, MenuItem } from "@blueprintjs/core";
+import React, { useState, useEffect } from "react";
+
+export function MySuggest(props) {
+ const {
+ items,
+ onChange,
+ onFilter = () => {},
+ initialQuery,
+ createNew = true,
+ ...rest
+ } = props;
+ const [selectedItem, setSelectedItem] = useState({});
+ const [query, setQuery] = useState("");
+
+ let itemz = [...items];
+
+ useEffect(() => {
+ if (initialQuery && initialQuery != "") {
+ setQuery(initialQuery);
+ itemz = [...itemz, { text: initialQuery }];
+ setSelectedItem(initialQuery);
+ }
+ }, [initialQuery]);
+
+ const itemRenderer = (item, itemProps) => {
+ const isSelected = item == selectedItem;
+ const { id, text } = item;
+ return (
+ : null}
+ intent={isSelected ? "primary" : null}
+ text={text}
+ onClick={itemProps.handleClick}
+ active={isSelected ? "active" : itemProps.modifiers.active}
+ />
+ );
+ };
+
+ const onQueryChange = (query) => {
+ onFilter(query);
+ };
+
+ const itemPredicate = (query, item) => {
+ const { id, text } = item;
+
+ return text.toLowerCase().indexOf(query.toLowerCase()) >= 0;
+ };
+
+ const onItemSelect = (item) => {
+ onChange(item);
+ setSelectedItem(item.text);
+ };
+
+ const createNewItemRenderer = (query, itemProps) => {
+ return (
+