aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAubrey Li <63608597+Aubrey-Li@users.noreply.github.com>2021-12-02 14:40:34 -0500
committerGitHub <noreply@github.com>2021-12-02 14:40:34 -0500
commitc415a692ed51563ea937901860d2532aede93d46 (patch)
treeaf24d05b564ca4e0011aee368a38eaedb7981d64
parentc2cd77ca1d2a67539f0af2a68c1e7336b3bc232b (diff)
parent2cb18e75aa487ff98086e15fef93e2f549c30496 (diff)
Merge pull request #50 from brown-dash/trails-aubrey
Maps integration
-rw-r--r--deploy/index.html1
-rw-r--r--maps.env1
-rw-r--r--package-lock.json90
-rw-r--r--package.json9
-rw-r--r--src/client/documents/DocumentTypes.ts43
-rw-r--r--src/client/documents/Documents.ts69
-rw-r--r--src/client/util/CurrentUserUtils.ts5
-rw-r--r--src/client/util/type_decls.d1
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/SidebarAnnos.tsx6
-rw-r--r--src/client/views/StyleProvider.tsx1
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx2
-rw-r--r--src/client/views/collections/CollectionMapView.tsx269
-rw-r--r--src/client/views/collections/CollectionMenu.tsx3
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/collections/MapView/CollectionMapView.scss (renamed from src/client/views/collections/CollectionMapView.scss)23
-rw-r--r--src/client/views/collections/MapView/CollectionMapView.tsx272
-rw-r--r--src/client/views/collections/TabDocView.scss3
-rw-r--r--src/client/views/collections/TabDocView.tsx4
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss62
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx691
-rw-r--r--src/client/views/nodes/PDFBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx5
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx12
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx4
-rw-r--r--src/client/views/search/IconBar.tsx2
-rw-r--r--src/client/views/search/IconButton.tsx1
-rw-r--r--src/fields/Doc.ts4
-rw-r--r--src/fields/URLField.ts1
-rw-r--r--src/fields/documentSchemas.ts2
-rw-r--r--src/server/ApiManagers/DownloadManager.ts2
-rw-r--r--src/server/ApiManagers/SearchManager.ts1
-rw-r--r--src/server/DashSession/Session/agents/process_message_router.ts2
-rw-r--r--src/server/GarbageCollector.ts2
-rw-r--r--src/server/remapUrl.ts3
-rw-r--r--src/server/updateProtos.ts2
-rw-r--r--src/server/websocket.ts1
39 files changed, 1276 insertions, 334 deletions
diff --git a/deploy/index.html b/deploy/index.html
index c32aff638..a236de872 100644
--- a/deploy/index.html
+++ b/deploy/index.html
@@ -11,6 +11,7 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
+
<script>
function getCookie(cname) {
var name = cname + "=";
diff --git a/maps.env b/maps.env
deleted file mode 100644
index 2b3b203e7..000000000
--- a/maps.env
+++ /dev/null
@@ -1 +0,0 @@
-DASH_GOOGLE_MAPS_API_KEY=AIzaSyAdJkThRhSivFE-O1I6RHSmRBWGstLY_sI \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 128d6fba8..288247869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -324,11 +324,18 @@
"integrity": "sha512-cY+QfDTbZ7XVxzx7jxbC98Oxr/zc7R2QpTLqTxqlfyXDrAJjzi/xUIqAUsygELB62JIrbsWxtSRhayKFkGI7MA=="
},
"@fortawesome/fontawesome-svg-core": {
- "version": "1.2.29",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.29.tgz",
- "integrity": "sha512-xmPmP2t8qrdo8RyKihTkGb09RnZoc+7HFBCnr0/6ZhStdGDSLeEd7ajV181+2W29NWIFfylO13rU+s3fpy3cnA==",
+ "version": "1.2.35",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz",
+ "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.29"
+ "@fortawesome/fontawesome-common-types": "^0.2.35"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz",
+ "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw=="
+ }
}
},
"@fortawesome/free-brands-svg-icons": {
@@ -355,21 +362,43 @@
}
},
"@fortawesome/free-solid-svg-icons": {
- "version": "5.13.1",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.1.tgz",
- "integrity": "sha512-LQH/0L1p4+rqtoSHa9qFYR84hpuRZKqaQ41cfBQx8b68p21zoWSekTAeA54I/2x9VlCHDLFlG74Nmdg4iTPQOg==",
+ "version": "5.15.3",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
+ "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.29"
+ "@fortawesome/fontawesome-common-types": "^0.2.35"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz",
+ "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw=="
+ }
}
},
"@fortawesome/react-fontawesome": {
- "version": "0.1.11",
- "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz",
- "integrity": "sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg==",
+ "version": "0.1.14",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz",
+ "integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==",
"requires": {
"prop-types": "^15.7.2"
}
},
+ "@googlemaps/js-api-loader": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.11.4.tgz",
+ "integrity": "sha512-atFTSgEXXsE7mOsOIInUKG4JApu/NdyZR79Ed6jF92MjVQTcD6dz/Xo8djf4cIetnq4WYQmetem0d8/uS4o8oQ==",
+ "requires": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "dependencies": {
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ }
+ }
+ },
"@hig/flyout": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@hig/flyout/-/flyout-1.2.1.tgz",
@@ -512,6 +541,27 @@
"react-is": "^16.8.0"
}
},
+ "@react-google-maps/api": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.2.0.tgz",
+ "integrity": "sha512-kMmzHyveLdEiLP+WN/H4vYEyaDTJn59mXj5Y0BxjJs5SIMhUtYxJ6EeJCV0aRSN+Gq5hYFLheBoy8Pfqtabq5g==",
+ "requires": {
+ "@googlemaps/js-api-loader": "1.11.4",
+ "@react-google-maps/infobox": "2.2.0",
+ "@react-google-maps/marker-clusterer": "2.2.0",
+ "invariant": "2.2.4"
+ }
+ },
+ "@react-google-maps/infobox": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.2.0.tgz",
+ "integrity": "sha512-nQbsPdcSTodXaaOgM9CaqyXytBkmollMuaYvERv5zxdoTWT1JvuViSaRbxpc3dYDsEHmp3myLx4ZbDcnOjf7Hg=="
+ },
+ "@react-google-maps/marker-clusterer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.2.0.tgz",
+ "integrity": "sha512-QaQAfY/FdaSwPmm0JnW9AW+t0ocfUYmuso9WmeIMwTRCqcqNxc4GFe5uT0LDLmsYt/wxAeYoGWVaD8sd/Vquig=="
+ },
"@react-three/fiber": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-6.0.16.tgz",
@@ -5574,9 +5624,9 @@
}
},
"dotenv": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
- "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
+ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
"dev": true
},
"double-bits": {
@@ -5810,8 +5860,8 @@
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"equation-editor-react": {
- "version": "git+ssh://git@github.com/bobzel/equation-editor-react.git#75915e852b4b36a6a4cd3e1cbc80598da6b65227",
- "from": "equation-editor-react@github:bobzel/equation-editor-react#useLocally",
+ "version": "github:bobzel/equation-editor-react#75915e852b4b36a6a4cd3e1cbc80598da6b65227",
+ "from": "github:bobzel/equation-editor-react#useLocally",
"requires": {
"jquery": "^3.4.1",
"mathquill": "^0.10.1-a"
@@ -8013,6 +8063,14 @@
"math-codegen": "^0.3.5"
}
},
+ "invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
diff --git a/package.json b/package.json
index 4f12f01d6..40504b9d8 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.1",
"css-loader": "^2.1.1",
- "dotenv": "^8.2.0",
+ "dotenv": "^8.6.0",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^1.6.0",
"jsdom": "^15.2.1",
@@ -116,15 +116,16 @@
"webpack-hot-middleware": "^2.24.3"
},
"dependencies": {
- "@fortawesome/fontawesome-svg-core": "^1.2.29",
+ "@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.14.0",
"@fortawesome/free-regular-svg-icons": "^5.13.1",
- "@fortawesome/free-solid-svg-icons": "^5.13.1",
- "@fortawesome/react-fontawesome": "^0.1.11",
+ "@fortawesome/free-solid-svg-icons": "^5.15.3",
+ "@fortawesome/react-fontawesome": "^0.1.14",
"@hig/flyout": "^1.2.1",
"@hig/theme-context": "^2.1.3",
"@hig/theme-data": "^2.16.1",
"@material-ui/core": "^4.11.0",
+ "@react-google-maps/api": "^2.2.0",
"@react-three/fiber": "^6.0.16",
"@types/bezier-js": "^4.1.0",
"@types/cors": "^2.8.8",
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index dba7ff907..161dff6e0 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -2,17 +2,17 @@ export enum DocumentType {
NONE = "none",
// core data types
- RTF = "rtf", // rich text
- IMG = "image", // image box
- WEB = "web", // web page or html clipping
- COL = "collection", // collection
- KVP = "kvp", // key value pane
- VID = "video", // video
- AUDIO = "audio", // audio
- PDF = "pdf", // pdf
- INK = "inks", // ink stroke
- SCREENSHOT = "screenshot", // view of a desktop application
- FONTICON = "fonticonbox", // font icon
+ RTF = "rtf",
+ IMG = "image",
+ WEB = "web",
+ COL = "collection",
+ KVP = "kvp",
+ VID = "video",
+ AUDIO = "audio",
+ PDF = "pdf",
+ INK = "inks",
+ SCREENSHOT = "screenshot",
+ FONTICON = "fonticonbox",
FILTER = "filter",
SEARCH = "search", // search query
LABEL = "label", // simple text label
@@ -23,19 +23,20 @@ export enum DocumentType {
SCRIPTING = "script", // script editor
EQUATION = "equation", // equation editor
FUNCPLOT = "funcplot", // function plotter
+ MAP = "map",
// special purpose wrappers that either take no data or are compositions of lower level types
- LINK = "link", // link (view of a document that acts as a link)
- LINKANCHOR = "linkanchor", // blue dot link anchor (view of a link document's anchor)
- IMPORT = "import", // directory import box (file system directory)
- SLIDER = "slider", // number slider (view of a number)
- PRES = "presentation", // presentation (view of a collection) --- shouldn't this be a view type? technically requires a special view in which documents must have their aliasOf fields filled in
- PRESELEMENT = "preselement",// presentation item (view of a document in a collection)
- COLOR = "color", // color picker (view of a color picker for a color string)
- YOUTUBE = "youtube", // youtube directory (view of you tube search results)
+ LINK = "link",
+ LINKANCHOR = "linkanchor",
+ IMPORT = "import",
+ SLIDER = "slider",
+ PRES = "presentation",
+ PRESELEMENT = "preselement",
+ COLOR = "color",
+ YOUTUBE = "youtube",
SEARCHITEM = "searchitem",
- COMPARISON = "comparison", // before/after view with slider (view of 2 images)
- GROUP = "group", // group of users
+ COMPARISON = "comparison",
+ GROUP = "group",
LINKDB = "linkdb", // database of links ??? why do we have this
SCRIPTDB = "scriptdb", // database of scripts
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 219f51a3a..5c5818f8f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -11,7 +11,7 @@ import { RichTextField } from "../../fields/RichTextField";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
+import { AudioField, ImageField, MapField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Upload } from "../../server/SharedMediaTypes";
import { OmitKeys, Utils } from "../../Utils";
@@ -60,6 +60,7 @@ import { SearchBox } from "../views/search/SearchBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { MapBox } from "../views/nodes/MapBox/MapBox";
const path = require('path');
const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", ""));
@@ -156,6 +157,9 @@ export class DocumentOptions {
x?: number;
y?: number;
z?: number; // whether document is in overlay (1) or not (0 or undefined)
+ lat?: number;
+ lng?: number;
+ infoWindowOpen?: boolean;
author?: string;
_layoutKey?: string;
unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)
@@ -399,6 +403,10 @@ export namespace Docs {
layout: { view: PDFBox, dataField: defaultDataKey },
options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any }
}],
+ [DocumentType.MAP, {
+ layout: { view: MapBox, dataField: defaultDataKey },
+ options: { _height: 600, _width: 800, links: ComputedField.MakeFunction("links(self)") as any }
+ }],
[DocumentType.IMPORT, {
layout: { view: DirectoryImportBox, dataField: defaultDataKey },
options: { _height: 150 }
@@ -776,6 +784,14 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options);
}
+ export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options);
+ }
+
+ export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id);
+ }
+
export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
}
@@ -806,7 +822,7 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id);
}
- export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
+ export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map });
}
@@ -1142,7 +1158,11 @@ export namespace DocUtils {
} else if (field instanceof List && field[0] instanceof Doc) {
created = Docs.Create.StackingDocument(DocListCast(field), resolved);
layout = CollectionView.LayoutString;
- } else {
+ } else if (field instanceof MapField) {
+ created = Docs.Create.MapDocument(DocListCast(field), resolved);
+ layout = MapBox.LayoutString;
+ }
+ else {
created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
layout = FormattedTextBox.LayoutString;
}
@@ -1173,6 +1193,12 @@ export namespace DocUtils {
if (!options._width) options._width = 400;
if (!options._height) options._height = (options._width as number) * 1200 / 927;
}
+ //TODO:al+glr
+ // if (type.indexOf("map") !== -1) {
+ // ctor = Docs.Create.MapDocument;
+ // if (!options._width) options._width = 800;
+ // if (!options._height) options._height = (options._width as number) * 3 / 4;
+ // }
if (type.indexOf("html") !== -1) {
if (path.includes(window.location.hostname)) {
const s = path.split('/');
@@ -1378,6 +1404,33 @@ export namespace DocUtils {
return optionsCollection;
}
+ // /**
+ // *
+ // * @param dms Degree Minute Second format exif gps data
+ // * @param ref ref that determines negativity of decimal coordinates
+ // * @returns a decimal format of gps latitude / longitude
+ // */
+ // function getDecimalfromDMS(dms?: number[], ref?: string) {
+ // if (dms && ref) {
+ // let degrees = dms[0] / dms[1];
+ // let minutes = dms[2] / dms[3] / 60.0;
+ // let seconds = dms[4] / dms[5] / 3600.0;
+
+ // if (['S', 'W'].includes(ref)) {
+ // degrees = -degrees; minutes = -minutes; seconds = -seconds
+ // }
+ // return (degrees + minutes + seconds).toFixed(5);
+ // }
+ // }
+
+ function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) {
+ var dd = degrees + minutes / 60 + seconds / (60 * 60);
+ if (direction == "S" || direction == "W") {
+ dd = dd * -1;
+ } // Don't do anything for N or E
+ return dd;
+ }
+
async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) {
if (result instanceof Error) {
alert(`Upload failed: ${result.message}`);
@@ -1400,6 +1453,16 @@ export namespace DocUtils {
proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
}
proto.contentSize = result.contentSize;
+ // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
+ const latitude = result.exifData?.data?.GPSLatitude;
+ const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
+ const longitude = result.exifData?.data?.GPSLongitude;
+ const longitudeDirection = result.exifData?.data?.GPSLongitudeRef;
+ if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) {
+ proto.lat = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection);
+ proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
+ }
+
}
generatedDocuments.push(doc);
}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b9e62b303..90b43c415 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -507,6 +507,10 @@ export class CurrentUserUtils {
if (doc.emptyWebpage === undefined) {
doc.emptyWebpage = Docs.Create.WebDocument("http://www.bing.com/", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
}
+ if (doc.emptyMap === undefined) {
+ doc.emptyMap = Docs.Create.MapDocument([], { title: "map", _showSidebar: true, _width: 800, _height: 600, system: true, cloneFieldFilter: new List<string>(["system"]) });
+ ((doc.emptyMap as Doc).proto as Doc)["dragFactory-count"] = 0;
+ }
if (doc.activeMobileMenu === undefined) {
this.setupActiveMobileMenu(doc);
}
@@ -526,6 +530,7 @@ export class CurrentUserUtils {
{ toolTip: "Tap to create a mobile view in a new pane, drag for a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc },
{ toolTip: "Tap to create a custom header note document, drag for a custom header note", title: "Custom", icon: "window-maximize", click: 'openOnRight(delegateDragFactory(this.dragFactory))', drag: 'delegateDragFactory(this.dragFactory)', dragFactory: doc.emptyHeader as Doc },
{ toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
+ { toolTip: "Tap to create a map in the new pane, drag for a map", title: "Map", icon: "map-marker-alt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyMap as Doc, noviceMode: true }
];
}
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index ac0bea46a..9063dc894 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -208,6 +208,7 @@ declare const Docs: {
PdfDocument(url: string, options?: DocumentOptions): Doc;
WebDocument(url: string, options?: DocumentOptions): Doc;
HtmlDocument(html: string, options?: DocumentOptions): Doc;
+ MapDocument(url: string, options?: DocumentOptions): Doc;
KVPDocument(document: Doc, options?: DocumentOptions): Doc;
FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc;
SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a9fea4a78..0bd6c9166 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -186,7 +186,7 @@ export class MainView extends React.Component {
fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical,
fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll,
fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines,
- fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen);
+ fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt);
this.initAuthenticationRouters();
}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 6a5b0a3ae..62f6e388f 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -23,6 +23,7 @@ interface ExtraProps {
layoutDoc: Doc;
rootDoc: Doc;
dataDoc: Doc;
+ // usePanelWidth: boolean;
showSidebar: boolean;
nativeWidth: number;
whenChildContentsActiveChanged: (isActive: boolean) => void;
@@ -77,7 +78,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
get sidebarKey() { return this.props.fieldKey + "-sidebar"; }
filtersHeight = () => 38;
screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1);
- panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth);
+ // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 :
+ // this.props.usePanelWidth ? this.props.PanelWidth() :
+ // (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
+ panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth);
panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey);
moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey);
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index f09d532ad..8ee673115 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -133,6 +133,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case DocumentType.IMG:
case DocumentType.WEB:
case DocumentType.PDF:
+ case DocumentType.MAP:
case DocumentType.SCREENSHOT:
case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
case DocumentType.COL:
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 66afad0ac..c7e62c15d 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -76,7 +76,7 @@ export class Timeline extends React.Component<FieldViewProps> {
*/
@computed
private get children(): Doc[] {
- const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF].includes(StrCast(this.props.Document.type) as any);
+ const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this.props.Document.type) as any);
if (annotatedDoc) {
return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"]);
}
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
deleted file mode 100644
index 2d7569d45..000000000
--- a/src/client/views/collections/CollectionMapView.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-import { GoogleApiWrapper, IMapProps, Map as GeoMap, Marker } from "google-maps-react";
-import { action, computed, Lambda, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast, Field, FieldResult, Opt } from "../../../fields/Doc";
-import { documentSchema } from "../../../fields/documentSchemas";
-import { Id } from "../../../fields/FieldSymbols";
-import { makeInterface } from "../../../fields/Schema";
-import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types";
-import { LinkManager } from "../../util/LinkManager";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import "./CollectionMapView.scss";
-import { CollectionSubView } from "./CollectionSubView";
-import React = require("react");
-import requestPromise = require("request-promise");
-
-type MapSchema = makeInterface<[typeof documentSchema]>;
-const MapSchema = makeInterface(documentSchema);
-
-export type LocationData = google.maps.LatLngLiteral & {
- address?: string
- resolvedAddress?: string;
- zoom?: number;
-};
-
-interface DocLatLng {
- lat: FieldResult<Field>;
- lng: FieldResult<Field>;
-}
-
-// Nowhere, Oklahoma
-const defaultLocation = { lat: 35.1592238, lng: -98.444512, zoom: 15 };
-const noResults = "ZERO_RESULTS";
-
-const query = async (data: string | google.maps.LatLngLiteral) => {
- const contents = typeof data === "string" ? `address=${data.replace(/\s+/g, "+")}` : `latlng=${data.lat},${data.lng}`;
- const target = `https://maps.googleapis.com/maps/api/geocode/json?${contents}&key=${process.env.GOOGLE_MAPS_GEO}`;
- try {
- return JSON.parse(await requestPromise.get(target));
- } catch {
- return undefined;
- }
-};
-
-@observer
-export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
-
- private _cancelAddrReq = new Map<string, boolean>();
- private _cancelLocReq = new Map<string, boolean>();
- private _initialLookupPending = new Map<string, boolean>();
- private responders: { locationDisposer: Lambda, addressDisposer: Lambda }[] = [];
-
- /**
- * Note that all the uses of runInAction below are not included
- * as a way to update observables (documents handle this already
- * in their property setters), but rather to create a single bulk
- * update and thus prevent uneeded invocations of the location-
- * and address–updating reactions.
- */
-
- private getLocation = (doc: Opt<Doc>, fieldKey: string, returnDefault: boolean = true): Opt<LocationData> => {
- if (doc) {
- const titleLoc = StrCast(doc.title).startsWith("@") ? StrCast(doc.title).substring(1) : undefined;
- const lat = Cast(doc[`${fieldKey}-lat`], "number", null) || (Cast(doc[`${fieldKey}-lat`], "string", null) && Number(Cast(doc[`${fieldKey}-lat`], "string", null))) || undefined;
- const lng = Cast(doc[`${fieldKey}-lng`], "number", null) || (Cast(doc[`${fieldKey}-lng`], "string", null) && Number(Cast(doc[`${fieldKey}-lng`], "string", null))) || undefined;
- const zoom = Cast(doc[`${fieldKey}-zoom`], "number", null) || (Cast(doc[`${fieldKey}-zoom`], "string", null) && Number(Cast(doc[`${fieldKey}-zoom`], "string", null))) || undefined;
- const address = titleLoc || StrCast(doc[`${fieldKey}-address`], StrCast(doc.title).replace(/^-/, ""));
- if (titleLoc || (address && (lat === undefined || lng === undefined))) {
- const id = doc[Id];
- if (!this._initialLookupPending.get(id)) {
- this._initialLookupPending.set(id, true);
- setTimeout(() => {
- titleLoc && Doc.SetInPlace(doc, `${fieldKey}-address`, titleLoc, true);
- this.respondToAddressChange(doc, fieldKey, address).then(() => this._initialLookupPending.delete(id));
- });
- }
- }
- return (lat === undefined || lng === undefined) ? (returnDefault ? defaultLocation : undefined) : { lat, lng, zoom };
- }
- return undefined;
- }
-
- private markerClick = async (layout: Doc, { lat, lng, zoom }: LocationData) => {
- const batch = UndoManager.StartBatch("marker click");
- const { fieldKey } = this.props;
- runInAction(() => {
- this.layoutDoc[`${fieldKey}-mapCenter-lat`] = lat;
- this.layoutDoc[`${fieldKey}-mapCenter-lng`] = lng;
- zoom && (this.layoutDoc[`${fieldKey}-mapCenter-zoom`] = zoom);
- });
- if (layout.isLinkButton && DocListCast(layout.links).length) {
- await LinkManager.traverseLink(undefined, layout, (doc: Doc, where: string, finished?: () => void) => {
- this.props.addDocTab(doc, where);
- finished?.();
- }, false, this.props.ContainingCollectionDoc, batch.end, undefined);
- } else {
- ScriptCast(layout.onClick)?.script.run({ this: layout, self: Cast(layout.rootDocument, Doc, null) || layout });
- batch.end();
- }
- }
-
- private renderMarkerIcon = (layout: Doc) => {
- const { Document } = this.props;
- const fieldKey = Doc.LayoutFieldKey(layout);
- const iconUrl = StrCast(layout.mapIconUrl, StrCast(Document.mapIconUrl));
- if (iconUrl) {
- const iconWidth = NumCast(layout[`${fieldKey}-iconWidth`], 45);
- const iconHeight = NumCast(layout[`${fieldKey}-iconHeight`], 45);
- const iconSize = new google.maps.Size(iconWidth, iconHeight);
- return {
- size: iconSize,
- scaledSize: iconSize,
- url: iconUrl
- };
- }
- }
-
- private renderMarker = (layout: Doc, fieldKey?: string) => {
- const location = this.getLocation(layout, fieldKey || Doc.LayoutFieldKey(layout));
- return !location ? (null) :
- <Marker
- key={layout[Id]}
- label={StrCast(layout[`${this.props.fieldKey}-address`])}
- position={location}
- onClick={() => this.markerClick(layout, location)}
- icon={this.renderMarkerIcon(layout)}
- />;
- }
-
- private respondToAddressChange = async (doc: Doc, fieldKey: string, newAddress: string, oldAddress?: string) => {
- if (newAddress === oldAddress) {
- return false;
- }
- const response = await query(newAddress);
- const id = doc[Id];
- if (!response || response.status === noResults) {
- this._cancelAddrReq.set(id, true);
- doc[`${fieldKey}-address`] = oldAddress;
- return false;
- }
- const { geometry, formatted_address } = response.results[0];
- const { lat, lng } = geometry.location;
- runInAction(() => {
- if (doc[`${fieldKey}-lat`] !== lat || doc[`${fieldKey}-lng`] !== lng) {
- this._cancelLocReq.set(id, true);
- Doc.SetInPlace(doc, `${fieldKey}-lat`, lat, true);
- Doc.SetInPlace(doc, `${fieldKey}-lng`, lng, true);
- }
- if (formatted_address !== newAddress) {
- this._cancelAddrReq.set(id, true);
- Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true);
- }
- });
- return true;
- }
-
- private respondToLocationChange = async (doc: Doc, fieldKey: string, newLatLng: DocLatLng, oldLatLng: Opt<DocLatLng>) => {
- if (newLatLng === oldLatLng) {
- return false;
- }
- const response = await query({ lat: NumCast(newLatLng.lat), lng: NumCast(newLatLng.lng) });
- const id = doc[Id];
- if (!response || response.status === noResults) {
- this._cancelLocReq.set(id, true);
- runInAction(() => {
- doc[`${fieldKey}-lat`] = oldLatLng?.lat;
- doc[`${fieldKey}-lng`] = oldLatLng?.lng;
- });
- return false;
- }
- const { formatted_address } = response.results[0];
- if (formatted_address !== doc[`${fieldKey}-address`]) {
- this._cancelAddrReq.set(doc[Id], true);
- Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true);
- }
- return true;
- }
-
- @computed get reactiveContents() {
- this.responders.forEach(({ locationDisposer, addressDisposer }) => {
- locationDisposer();
- addressDisposer();
- });
- this.responders = [];
- return this.childLayoutPairs.map(({ layout }) => {
- const fieldKey = Doc.LayoutFieldKey(layout);
- const id = layout[Id];
- this.responders.push({
- locationDisposer: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] }))
- .observe(({ oldValue, newValue }) => {
- if (this._cancelLocReq.get(id)) {
- this._cancelLocReq.set(id, false);
- } else if (newValue.lat !== undefined && newValue.lng !== undefined) {
- this.respondToLocationChange(layout, fieldKey, newValue, oldValue);
- }
- }),
- addressDisposer: computed(() => Cast(layout[`${fieldKey}-address`], "string", null))
- .observe(({ oldValue, newValue }) => {
- if (this._cancelAddrReq.get(id)) {
- this._cancelAddrReq.set(id, false);
- } else if (newValue?.length) {
- this.respondToAddressChange(layout, fieldKey, newValue, oldValue);
- }
- })
- });
- return this.renderMarker(layout);
- });
- }
-
- render() {
- const { childLayoutPairs } = this;
- const { Document, fieldKey, isContentActive: active, google } = this.props;
- const mapLoc = this.getLocation(this.rootDoc, `${fieldKey}-mapCenter`, false);
- let center = mapLoc;
- if (center === undefined) {
- const childLocations = childLayoutPairs.map(({ layout }) => this.getLocation(layout, Doc.LayoutFieldKey(layout), false));
- center = childLocations.find(location => location) || defaultLocation;
- }
- return <div className="collectionMapView" ref={this.createDashEventsTarget}>
- <div className={"collectionMapView-contents"}
- style={{ pointerEvents: active() ? undefined : "none" }}
- onWheel={e => e.stopPropagation()}
- onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} >
- <GeoMap
- google={google}
- zoom={center.zoom || 10}
- initialCenter={center}
- center={center}
- onIdle={(_props?: IMapProps, map?: google.maps.Map) => {
- if (this.layoutDoc._lockedTransform) {
- // reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead)
- map?.setZoom(center?.zoom || 10);
- map?.setCenter({ lat: center?.lat!, lng: center?.lng! });
- } else {
- const zoom = map?.getZoom();
- (center?.zoom !== zoom) && undoBatch(action(() => {
- Document[`${fieldKey}-mapCenter-zoom`] = zoom;
- }))();
- }
- }}
- onDragend={(_props?: IMapProps, map?: google.maps.Map) => {
- if (this.layoutDoc._lockedTransform) {
- // reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead)
- map?.setCenter({ lat: center?.lat!, lng: center?.lng! });
- } else {
- undoBatch(action(({ lat, lng }) => {
- Document[`${fieldKey}-mapCenter-lat`] = lat();
- Document[`${fieldKey}-mapCenter-lng`] = lng();
- }))(map?.getCenter());
- }
- }}
- >
- {this.reactiveContents}
- {mapLoc && StrCast(this.rootDoc[`${fieldKey}-mapCenter-address`]) ? this.renderMarker(this.rootDoc, `${fieldKey}-mapCenter`) : undefined}
- </GeoMap>
- </div>
- </div>;
- }
-
-}
-
-export default GoogleApiWrapper({
- apiKey: process.env.GOOGLE_MAPS!,
- LoadingContainer: () => {
- console.log(process.env.GOOGLE_MAPS);
- return <div className={"loadingWrapper"}>
- <img className={"loadingGif"} src={"/assets/loading.gif"} />
- </div>;
- }
-})(CollectionMapView) as any; \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 2c2d5dc75..131f5ba46 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -346,6 +346,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
case DocumentType.WEB: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />);
case DocumentType.VID: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />);
case DocumentType.RTF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} isDoc={true} />);
+ case DocumentType.MAP: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />);
}
}
@@ -499,7 +500,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
const scroll = targetDoc._scrollTop;
activeDoc.presPinView = true;
activeDoc.presPinViewScroll = scroll;
- } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) {
+ } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) {
const x = targetDoc._panX;
const y = targetDoc._panY;
const scale = targetDoc._viewScale;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 8e84b59de..3ad9333de 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -25,7 +25,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
import { CollectionGridView } from './collectionGrid/CollectionGridView';
import { CollectionLinearView } from './collectionLinear';
-import CollectionMapView from './CollectionMapView';
+import CollectionMapView from './MapView/CollectionMapView';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
import { CollectionPileView } from './CollectionPileView';
diff --git a/src/client/views/collections/CollectionMapView.scss b/src/client/views/collections/MapView/CollectionMapView.scss
index 870b7fda8..0dc226c04 100644
--- a/src/client/views/collections/CollectionMapView.scss
+++ b/src/client/views/collections/MapView/CollectionMapView.scss
@@ -1,13 +1,34 @@
.collectionMapView {
width: 100%;
height: 100%;
+ overflow: hidden;
.collectionMapView-contents {
width: 100%;
height: 100%;
+ overflow: hidden;
> div {
position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box
}
+
+ .map-wrapper {
+ .searchbox {
+ box-sizing: border-box;
+ border: 1px solid transparent;
+ width: 300px;
+ height: 32px;
+ padding: 0 12px;
+ border-radius: 3px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ font-size: 14px;
+ outline: none;
+ text-overflow: ellipses;
+ position: absolute;
+ left: 50%;
+ margin-left: -120px;
+ margin-top: 5px;
+ }
+ }
}
}
@@ -27,4 +48,4 @@
width: 50px;
height: 50px;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/MapView/CollectionMapView.tsx b/src/client/views/collections/MapView/CollectionMapView.tsx
new file mode 100644
index 000000000..885f51bfb
--- /dev/null
+++ b/src/client/views/collections/MapView/CollectionMapView.tsx
@@ -0,0 +1,272 @@
+import { GoogleMap, Marker, InfoWindow, InfoBox, useJsApiLoader, LoadScript, GoogleMapProps, StandaloneSearchBox, Autocomplete } from '@react-google-maps/api';
+import { observable, action, computed, Lambda, runInAction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, Field, FieldResult, Opt } from "../../../../fields/Doc";
+import { documentSchema } from "../../../../fields/documentSchemas";
+import { Id } from "../../../../fields/FieldSymbols";
+import { makeInterface } from "../../../../fields/Schema";
+import { Cast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { LinkManager } from "../../../util/LinkManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import "./CollectionMapView.scss";
+import { CollectionSubView } from "../CollectionSubView";
+import React = require("react");
+import requestPromise = require("request-promise");
+import ReactDOM from 'react-dom';
+import { DragManager } from '../../../util/DragManager';
+import { MapMarker } from '../../nodes/MapBox/MapMarker';
+
+
+/**
+ * Idea behind storing a marker:
+ * 1. on the map api, have a button "add marker" that adds the marker on the map & store the marker as a node in the collection
+ * (but don't render the marker in the collection itself)
+ * 2. each marker, as a node, has the same feature as all other nodes for linking (the same way one could form a link between a node inside a child collection
+ * and a node outside the child collection)
+ *
+ * /util/LinkManager.ts -- link relations
+ *
+ */
+
+type MapSchema = makeInterface<[typeof documentSchema]>;
+const MapSchema = makeInterface(documentSchema);
+
+export type Coordinates = {
+ lat: number,
+ lng: number,
+}
+
+export type LocationData = {
+ id: string;
+ pos: Coordinates;
+};
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 38.685,
+ lng: -115.234,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+}
+
+// const drawingManager = new google.maps.drawing.DrawingManager({
+// drawingControl: true,
+// drawingControlOptions: {
+// position: google.maps.ControlPosition.TOP_RIGHT,
+// drawingModes: [
+// google.maps.drawing.OverlayType.MARKER,
+// // currently we are not supporting the following drawing mode on map, a thought for future development
+// // google.maps.drawing.OverlayType.CIRCLE,
+// // google.maps.drawing.OverlayType.POLYLINE,
+// ],
+// },
+// });
+
+const options = {
+ fields: ["formatted_address", "geometry", "name"], // note: level of details is charged by item per retrieval, not recommended to return all fields
+ strictBounds: false,
+ types: ["establishment"], // type pf places, subject of change according to user need
+} as google.maps.places.AutocompleteOptions;
+
+@observer
+export default class CollectionMapView extends CollectionSubView<MapSchema, Partial<GoogleMapProps>>(MapSchema) {
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+
+
+ @observable private _map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: LocationData | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ @observable private center = defaultCenter;
+ @observable private zoom = 2.5;
+ @observable private infoWindowOpen = false;
+ @observable private bounds = new window.google.maps.LatLngBounds();
+ @observable private inputRef = React.createRef<HTMLInputElement>();
+ @observable private searchMarkers: google.maps.Marker[] = [];
+ @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
+
+ @observable private myPlaces: LocationData[] = [
+ { id: "id1", pos: { lat: 39.09366509575983, lng: -94.58751660204751 } },
+ { id: "id2", pos: { lat: 41.82399, lng: -71.41283 } },
+ { id: "id3", pos: { lat: 47.606214, lng: -122.33207 } },
+ ];
+
+ @action
+ private setSearchBox = (searchBox: any) => {
+ this.searchBox = searchBox;
+ }
+
+ // iterate myPlaces to size, center, and zoom map to contain all markers
+ private fitBounds = (map: google.maps.Map) => {
+ console.log('map bound is:' + this.bounds);
+ this.myPlaces ? this.myPlaces.map(place => {
+ this.bounds.extend(place.pos!);
+ }) : null;
+ map.fitBounds(this.bounds);
+ }
+
+ // store a reference to google map instance; fit map bounds to contain all markers
+ @action
+ private loadHandler = (map: google.maps.Map) => {
+ this._map = map;
+ drawingManager.setMap(map);
+ this.fitBounds(map);
+ }
+
+ @action
+ private markerClickHandler = (e: MouseEvent, place: any) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+
+ console.log(this.selectedPlace);
+
+ // used so clicking a second marker works
+ if (this.infoWindowOpen) {
+ this.infoWindowOpen = false;
+ console.log("closeinfowindow")
+ }
+ this.infoWindowOpen = true;
+ console.log("open infowindow")
+ }
+
+ @action
+ private handleInfoWindowClose = () => {
+ if (this.infoWindowOpen) {
+ this.infoWindowOpen = false;
+ }
+ this.infoWindowOpen = false;
+ this.selectedPlace = undefined;
+ }
+
+ @action
+ private handlePlaceChanged = () => {
+ console.log(this.searchBox);
+ const place = this.searchBox.getPlace();
+
+ if (!place.geometry || !place.geometry.location) {
+ // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed
+ window.alert("No details available for input: '" + place.name + "'");
+ return;
+ }
+
+ // zoom in on the location of the search result
+ if (place.geometry.viewport) {
+ console.log(this._map);
+ this._map.fitBounds(place.geometry.viewport);
+ } else {
+ console.log(this._map);
+ this._map.setCenter(place.geometry.location);
+ this._map.setZoom(17);
+ }
+
+ // customize icon => customized icon for the nature of the location selected
+ const icon = {
+ url: place.icon as string,
+ size: new google.maps.Size(71, 71),
+ origin: new google.maps.Point(0, 0),
+ anchor: new google.maps.Point(17, 34),
+ scaledSize: new google.maps.Size(25, 25),
+ };
+
+ // put temporary cutomized marker on searched location
+ this.searchMarkers.forEach((marker) => {
+ marker.setMap(null);
+ });
+ this.searchMarkers = [];
+ this.searchMarkers.push(
+ new window.google.maps.Marker({
+ map: this._map,
+ icon,
+ title: place.name,
+ position: place.geometry.location,
+ })
+ )
+ }
+
+
+ @action
+ private addMarker = (location: google.maps.LatLng | undefined, map: google.maps.Map) => {
+ new window.google.maps.Marker({
+ position: location,
+ map: map
+ });
+ }
+
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: LocationData) => {
+ place.id ? this.markerMap[place.id] = marker : null;
+ }
+
+ render() {
+ const { Document, fieldKey, isContentActive: active } = this.props;
+
+ return <div className="collectionMapView" ref={this.createDashEventsTarget}>
+
+ <div className={"collectionMapView-contents"}
+ style={{ pointerEvents: active() ? undefined : "none", overflow: 'hidden' }}
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} >
+ {/* <LoadScript
+ googleMapsApiKey={process.env.GOOGLE_MAPS!}
+ libraries={['places', 'drawing']}
+ > */}
+ <div className="map-wrapper">
+ <GoogleMap
+ mapContainerStyle={mapContainerStyle}
+ zoom={this.zoom}
+ center={this.center}
+ onLoad={map => this.loadHandler(map)}
+ options={mapOptions}
+ >
+ <Autocomplete
+ onLoad={this.setSearchBox}
+ onPlaceChanged={this.handlePlaceChanged}>
+ <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" />
+ </Autocomplete>
+
+ {this.myPlaces ? this.myPlaces.map(place =>
+ <Marker
+ position={place.pos}
+ onLoad={marker => this.markerLoadHandler(marker, place)}
+ onClick={e => this.markerClickHandler(e, place)}
+ draggable={false}
+ />
+ ) : null}
+ {this.infoWindowOpen && this.selectedPlace && (
+ <InfoWindow
+ anchor={this.markerMap[this.selectedPlace.id!]}
+ onCloseClick={this.handleInfoWindowClose}
+ >
+ <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}>
+ <div style={{ fontSize: 16 }}>
+ <div>
+ <img src="http://placekitten.com/200/300" />
+ <hr />
+ <form>
+ <label>Title: </label><br />
+ <input type="text" id="fname" name="fname"></input><br />
+ <label>Desription: </label><br />
+ <textarea style={{ height: 150 }} id="lname" name="lname" placeholder="Notes, a short description of this location, a brief comment, etc."></textarea>
+ </form>
+ <hr />
+ <div>
+ <button>New link+</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </InfoWindow>
+ )}
+ </GoogleMap>
+ </div>
+ {/* </LoadScript > */}
+ </div >
+ </div >;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index 7f62ecaa0..0d045bada 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -66,7 +66,6 @@ input.lm_title {
right: 15;
bottom: 15;
border: solid 1px;
- box-shadow: black 0.4vw 0.4vw 0.8vw;
width: 100%;
height: 100%;
transition: all 0.5s;
@@ -101,4 +100,4 @@ input.lm_title {
&:hover {
box-shadow: none;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5ba019698..7e57d0e89 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -437,8 +437,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
case StyleProp.PointerEvents: return "none";
case StyleProp.DocContents:
const background = doc.type === DocumentType.PDF ? "red" : doc.type === DocumentType.IMG ? "blue" : doc.type === DocumentType.RTF ? "orange" :
- doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : "gray";
- return doc.type === DocumentType.COL || doc.type === DocumentType.INK ?
+ doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : doc.type === DocumentType.MAP ? "blue" : "gray";
+ return doc.type === DocumentType.COL ?
undefined :
<div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: "absolute", display: "block", background }} />;
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index dbab5e762..005133eb0 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -40,6 +40,7 @@ import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import React = require("react");
import XRegExp = require("xregexp");
+import { MapBox } from "./MapBox/MapBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -225,7 +226,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
- ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox,
+ ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox,
ScreenshotBox,
HTMLtag, ComparisonBox
}}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c8a32a911..138bad9b8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -829,7 +829,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
return this.props.isContentActive() === false ? false : (
CurrentUserUtils.SelectedTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
- this.props.rootSelected() ||
+ this.rootSelected() ||
this.props.Document.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
new file mode 100644
index 000000000..f275bed54
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -0,0 +1,62 @@
+@import "../../global/globalCssVariables.scss";
+.mapBox {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ display: flex;
+ > div {
+ position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box
+ }
+
+ .mapBox-overlayButton-sidebar {
+ background: #121721;
+ height: 25px;
+ width: 25px;
+ right: 5px;
+ display: flex;
+ position: absolute;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ pointer-events: all;
+ z-index: 1; // so it appears on top of the document's title, if shown
+
+ box-shadow: $standard-box-shadow;
+ transition: 0.2s;
+
+ &:hover{
+ filter: brightness(0.85);
+ }
+ }
+
+ .mapBox-wrapper {
+ width: 100%;
+ .searchbox {
+ box-sizing: border-box;
+ border: 1px solid transparent;
+ width: 240px;
+ height: 32px;
+ padding: 0 12px;
+ border-radius: 3px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ font-size: 14px;
+ outline: none;
+ text-overflow: ellipses;
+ position: absolute;
+ left: 50%;
+ margin-left: -120px;
+ }
+ }
+
+ .mapBox-sidebar-handle {
+ position: absolute !important;
+ top: 0;
+ //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views
+ width: 10px;
+ height: 100%;
+ max-height: 35px;
+ background: lightgray;
+ border-radius: 20px;
+ cursor:grabbing;
+ }
+}
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
new file mode 100644
index 000000000..7875060e2
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -0,0 +1,691 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, LoadScript, Marker } from '@react-google-maps/api';
+import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx';
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { Id } from '../../../../fields/FieldSymbols';
+import { InkTool } from '../../../../fields/InkField';
+import { makeInterface } from '../../../../fields/Schema';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { TraceMobx } from '../../../../fields/util';
+import { emptyFunction, OmitKeys, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
+import { DragManager } from '../../../util/DragManager';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
+import { CollectionFreeFormView, MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { CollectionStackingView } from '../../collections/CollectionStackingView';
+import { CollectionViewType } from '../../collections/CollectionView';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { Annotation } from '../../pdf/Annotation';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { StyleProp } from '../../StyleProvider';
+import { FieldView, FieldViewProps } from '../FieldView';
+import * as dotenv from 'dotenv';
+import "./MapBox.scss";
+
+/**
+ * MapBox architecture:
+ * Main component: MapBox.tsx
+ * Supporting Components: SidebarAnnos, CollectionStackingView
+ *
+ * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
+ * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
+ * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
+ * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
+ * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
+ * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
+ */
+
+// const _global = (window /* browser */ || global /* node */) as any;
+
+type MapDocument = makeInterface<[typeof documentSchema]>;
+const MapDocument = makeInterface(documentSchema);
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 38.685,
+ lng: -115.234,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+}
+
+dotenv.config({ path: __dirname + '/.env' })
+const apiKey = process.env.GOOGLE_MAPS;
+
+const script = document.createElement('script');
+script.defer = true;
+script.async = true;
+script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+document.head.appendChild(script);
+
+/**
+ * Consider integrating later: allows for drawing, circling, making shapes on map
+ */
+// const drawingManager = new window.google.maps.drawing.DrawingManager({
+// drawingControl: true,
+// drawingControlOptions: {
+// position: google.maps.ControlPosition.TOP_RIGHT,
+// drawingModes: [
+// google.maps.drawing.OverlayType.MARKER,
+// // currently we are not supporting the following drawing mode on map, a thought for future development
+// google.maps.drawing.OverlayType.CIRCLE,
+// google.maps.drawing.OverlayType.POLYLINE,
+// ],
+// },
+// });
+
+
+// options for searchbox in Google Maps Places Autocomplete API
+const options = {
+ fields: ["formatted_address", "geometry", "name"], // note: level of details is charged by item per retrieval, not recommended to return all fields
+ strictBounds: false,
+ types: ["establishment"], // type pf places, subject of change according to user need
+} as google.maps.places.AutocompleteOptions;
+
+@observer
+export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>, MapDocument>(MapDocument) {
+
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ @observable private _overlayAnnoInfo: Opt<Doc>;
+ showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); }
+ public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ @computed get inlineTextAnnotations() { return this.allMapMarkers.filter(a => a.textInlineAnnotations); }
+
+ @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: Doc | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ // @observable private markerIdToMapMarker: { [id: string]: Doc | MapMarker | undefined } = {};
+ @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
+ @observable private zoom = 2.5;
+ @observable private _marqueeing: number[] | undefined;
+ @observable private _isAnnotating = false;
+ @observable private bounds = new window.google.maps.LatLngBounds();
+ @observable private inputRef = React.createRef<HTMLInputElement>();
+ @observable private searchMarkers: google.maps.Marker[] = [];
+ @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
+ @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); };
+ @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); };
+ @observable private toggleAddMarker = false;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+
+ static _canAnnotate = true;
+ static _hadSelection: boolean = false;
+ private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ @action
+ private setSearchBox = (searchBox: any) => {
+ this.searchBox = searchBox;
+ }
+
+ // iterate allMarkers to size, center, and zoom map to contain all markers
+ private fitBounds = (map: google.maps.Map) => {
+ console.log('map bound is:' + this.bounds);
+ this.allMapMarkers.map(place => {
+ this.bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) });
+ });
+ map.fitBounds(this.bounds)
+ }
+
+ /**
+ * Custom control for add marker button
+ * @param controlDiv
+ * @param map
+ */
+ private CenterControl = (controlDiv: Element) => {
+ // Set CSS for the control border.
+ const controlUI = document.createElement("div");
+
+ controlUI.style.backgroundColor = "#fff";
+ controlUI.style.border = "2px solid #fff";
+ controlUI.style.borderRadius = "3px";
+ controlUI.style.cursor = "pointer";
+ controlUI.style.marginTop = "8px";
+ controlUI.style.marginBottom = "22px";
+ controlUI.style.textAlign = "center";
+ controlUI.title = "Click to add marker to the location your pointer is at";
+ controlDiv.appendChild(controlUI);
+
+ // Set CSS for the control interior.
+ const controlText = document.createElement("div");
+
+ controlText.style.color = "rgb(25,25,25)";
+ controlText.style.fontFamily = "Roboto,Arial,sans-serif";
+ controlText.style.fontSize = "16px";
+ controlText.style.lineHeight = "38px";
+ controlText.style.paddingLeft = "5px";
+ controlText.style.paddingRight = "5px";
+ controlText.innerHTML = "Add Marker";
+ controlUI.appendChild(controlText);
+
+ // Setup the click event listeners
+ controlUI.addEventListener("click", () => {
+ if (this.toggleAddMarker == true) {
+ this.toggleAddMarker = false;
+ console.log("add marker button status:" + this.toggleAddMarker);
+ controlUI.style.backgroundColor = "#fff";
+ controlText.style.color = "rgb(25,25,25)";
+ } else {
+ this.toggleAddMarker = true;
+ console.log("add marker button status:" + this.toggleAddMarker);
+ controlUI.style.backgroundColor = "#4476f7";
+ controlText.style.color = "rgb(255,255,255)";
+ };
+ });
+ }
+
+ /**
+ * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list
+ * @param position - the LatLng position where the marker is placed
+ * @param map
+ */
+ @action
+ private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
+ const marker = new google.maps.Marker({
+ position: position,
+ map: map
+ });
+ map.panTo(position);
+ const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
+ this.addDocument(mapMarker, this.annotationKey);
+ }
+
+
+ /**
+ * store a reference to google map instance
+ * setup the drawing manager on the top right corner of map
+ * fit map bounds to contain all markers
+ * @param map
+ */
+ @action
+ private loadHandler = (map: google.maps.Map) => {
+ this._map = map;
+ const centerControlDiv = document.createElement("div");
+ this.CenterControl(centerControlDiv);
+ map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv);
+ //drawingManager.setMap(map);
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ (position: GeolocationPosition) => {
+ const pos = {
+ lat: position.coords.latitude,
+ lng: position.coords.longitude,
+ };
+ this._map.setCenter(pos);
+ }
+ );
+ } else {
+ alert("Your geolocation is not supported by browser.")
+ };
+ console.log("all sidebar docs during map loading is:")
+ console.log(this.allSidebarDocs);
+ this.fitBounds(map);
+
+
+ // listener to addmarker event
+ this._map.addListener('click', (e) => {
+ console.log("add marker map status:" + this.toggleAddMarker);
+ if (this.toggleAddMarker == true) {
+ this.placeMarker(e.latLng, map)
+ console.log(this.allMapMarkers)
+ }
+ })
+ }
+
+ /**
+ * Load and render all map markers
+ * @param marker
+ * @param place
+ */
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
+ place[Id] ? this.markerMap[place[Id]] = marker : null;
+
+ console.log("the following is a markerMap from id to Marker:")
+ console.log(this.markerMap);
+ }
+
+ /**
+ * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true
+ * @param e
+ * @param place
+ */
+ @action
+ private markerClickHandler = (e: MouseEvent, place: Doc) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+
+ console.log("you have selected this location:");
+ console.log(this.selectedPlace);
+
+ place.infoWindowOpen = true;
+ console.log("open infowindow")
+ }
+
+ /**
+ * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ console.log("print all sidebar Docs");
+ console.log(this.allSidebarDocs);
+ if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc
+ docs.forEach(doc => {
+ if (doc.lat !== undefined && doc.lng !== undefined) {
+ const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng == doc.lng);
+ doc.onClickBehavior = "enterPortal";
+ if (existingMarker) {
+ Doc.AddDocToList(existingMarker, "data", doc);
+ } else {
+ const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
+ this.addDocument(marker, this.annotationKey);
+ }
+ }
+ }) //add to annotation list
+ console.log("sidebaraddDocument");
+ console.log(doc);
+
+ return this.addDocument(doc, sidebarKey); // add to sidebar list
+ }
+
+ /**
+ * Removing documents from the sidebar
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ if (this.layoutDoc._showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ console.log(this.allMapMarkers);
+ console.log(this.allSidebarDocs);
+ })
+ return this.removeDocument(doc, sidebarKey);
+ }
+
+ /**
+ * Toggle sidebar onclick the tiny comment button on the top right corner
+ * @param e
+ */
+ sidebarBtnDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e, down, delta) => {
+ const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
+ const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
+ const ratio = (curNativeWidth + localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth;
+ if (ratio >= 1) {
+ this.layoutDoc.nativeWidth = nativeWidth * ratio;
+ this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0];
+ this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ }
+ return false;
+ }, emptyFunction, this.toggleSidebar);
+ }
+
+ sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
+ @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
+ @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
+
+
+ /**
+ * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted;
+ * add a customized temporary marker on the map
+ */
+ @action
+ private handlePlaceChanged = () => {
+ console.log(this.searchBox);
+ const place = this.searchBox.getPlace();
+
+ if (!place.geometry || !place.geometry.location) {
+ // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed
+ window.alert("No details available for input: '" + place.name + "'");
+ return;
+ }
+
+ // zoom in on the location of the search result
+ if (place.geometry.viewport) {
+ console.log(this._map);
+ this._map.fitBounds(place.geometry.viewport);
+ } else {
+ console.log(this._map);
+ this._map.setCenter(place.geometry.location);
+ this._map.setZoom(17);
+ }
+
+ // customize icon => customized icon for the nature of the location selected
+ const icon = {
+ url: place.icon as string,
+ size: new google.maps.Size(71, 71),
+ origin: new google.maps.Point(0, 0),
+ anchor: new google.maps.Point(17, 34),
+ scaledSize: new google.maps.Size(25, 25),
+ };
+
+ // put temporary cutomized marker on searched location
+ this.searchMarkers.forEach((marker) => {
+ marker.setMap(null);
+ });
+ this.searchMarkers = [];
+ this.searchMarkers.push(
+ new window.google.maps.Marker({
+ map: this._map,
+ icon,
+ title: place.name,
+ position: place.geometry.location,
+ })
+ )
+ }
+
+
+ @action
+ private handleInfoWindowClose = (place: Doc) => {
+ if (place.infoWindowOpen) {
+ place.infoWindowOpen = false;
+ }
+ place.infoWindowOpen = false;
+ }
+
+ /**
+ * Handles toggle of sidebar on click the little comment button
+ */
+ @computed get sidebarHandle() {
+ TraceMobx();
+ const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
+ const color = !annotated ? Colors.WHITE : Colors.BLACK;
+ const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+ return (!annotated) ? (null) :
+ <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
+ style={{
+ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`,
+ backgroundColor: backgroundColor,
+ color: color,
+ opacity: annotated ? 1 : undefined
+ }} >
+ <FontAwesomeIcon icon={"comment-alt"} />
+ </div>;
+ }
+
+ // TODO: Adding highlight box layer to Maps
+ @action
+ toggleSidebar = () => {
+ const prevWidth = this.sidebarWidth();
+ this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
+ this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ }
+
+ sidebarDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
+ }
+ sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const bounds = this._ref.current!.getBoundingClientRect();
+ this.layoutDoc._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%";
+ this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%";
+ e.preventDefault();
+ return false;
+ }
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
+
+ @action
+ onMarqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
+ @action finishMarquee = (x?: number, y?: number) => {
+ this._marqueeing = undefined;
+ this._isAnnotating = false;
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ }
+
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ return this.addDocument(doc, annotationKey);
+ }
+
+ pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none";
+
+ @computed get annotationLayer() {
+ TraceMobx();
+ const pe = this.pointerEvents();
+ return <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
+ <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ </div>;
+
+ }
+
+
+ getAnchor = () => {
+ const anchor =
+ AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
+ this.rootDoc
+ return anchor;
+ }
+
+ infoWidth = () => this.props.PanelWidth() / 5;
+ infoHeight = () => this.props.PanelWidth() / 5;
+
+ // Collection stacking view for documents in the infowindow of a map marker
+ private renderChildDocs = (selectedDoc: Doc) => {
+ return <div style={{ width: this.infoWidth(), height: this.infoHeight() }}>
+ <CollectionStackingView {
+ ...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ Document={selectedDoc}
+ DataDoc={undefined}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ PanelHeight={this.infoHeight}
+ PanelWidth={this.infoWidth}
+ docFilters={returnEmptyFilter}
+ setHeight={emptyFunction}
+ isAnnotationOverlay={false}
+ select={emptyFunction}
+ scaling={returnOne}
+ isContentActive={returnTrue}
+ chromeHidden={true}
+ rootSelected={returnFalse}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ childHideDecorationTitle={returnTrue}
+ // childDocumentsActive={returnFalse}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ viewType={CollectionViewType.Stacking}
+ fieldKey={"data"}
+ pointerEvents={"all"}
+ /></div>;
+ }
+
+ /**
+ * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
+ * @returns
+ */
+ private renderMarkers = () => {
+ return this.allMapMarkers.map(place => (
+ <Marker
+ key={place[Id]}
+ position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }}
+ onLoad={marker => this.markerLoadHandler(marker, place)}
+ onClick={e => this.markerClickHandler(e, place)}
+ />
+ ))
+ }
+
+ /**
+ * Renders infowindow corresponding to a map marker document
+ * @param place
+ * @returns
+ */
+ private renderInfoWindow = (place: Doc) => {
+
+ return place.infoWindowOpen && (
+ <InfoWindow
+ key={place[Id]}
+ anchor={this.markerMap[place[Id]]}
+ onCloseClick={() => this.handleInfoWindowClose(place)}
+ >
+ <div className="mapbox-infowindow" style={{ backgroundColor: 'white', opacity: 0.75, padding: 12, fontSize: 17 }}>
+ {this.renderChildDocs(place)}
+ <hr />
+ <div>
+ <button>New link+</button>
+ </div>
+ </div>
+ </InfoWindow>
+ )
+ }
+
+ // TODO: auto center on select a document in the sidebar
+ private handleMapCenter = (map: google.maps.Map) => {
+ console.log("print the selected views in selectionManager:")
+ if (SelectionManager.Views().lastElement()) {
+ console.log(SelectionManager.Views().lastElement());
+ }
+ }
+
+ panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
+ panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+ transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+
+ anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+
+ render() {
+ const renderAnnotations = (docFilters?: () => string[]) =>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ renderDepth={this.props.renderDepth + 1}
+ isAnnotationOverlay={true}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.scrollXf}
+ scaling={returnOne}
+ dropAction={"alias"}
+ docFilters={docFilters || this.props.docFilters}
+ dontRenderDocuments={docFilters ? false : true}
+ select={emptyFunction}
+ ContentScaling={returnOne}
+ bringToFront={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.sidebarAddDocument}
+ childPointerEvents={true}
+ pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ return <div className="mapBox" ref={this._ref}>
+ {console.log(apiKey)}
+ {/* <LoadScript
+ googleMapsApiKey={apiKey!}
+ libraries={['places', 'drawing']}
+ > */}
+ <div className="mapBox-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()}
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+
+ <div style={{ mixBlendMode: "multiply" }}>
+ {renderAnnotations(this.transparentFilter)}
+ </div>
+ {renderAnnotations(this.opaqueFilter)}
+ {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()}
+ {this.annotationLayer}
+ <GoogleMap
+ mapContainerStyle={mapContainerStyle}
+ zoom={this.zoom}
+ onLoad={map => this.loadHandler(map)}
+ options={mapOptions}
+ >
+ <Autocomplete
+ onLoad={this.setSearchBox}
+ onPlaceChanged={this.handlePlaceChanged}>
+ <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" />
+ </Autocomplete>
+
+ {this.renderMarkers()}
+ {this.allMapMarkers.map(place => (
+ this.renderInfoWindow(place)
+ ))}
+ {this.handleMapCenter(this._map)}
+ </GoogleMap>
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+ <MarqueeAnnotator rootDoc={this.rootDoc}
+ anchorMenuClick={this.anchorMenuClick}
+ scrollTop={0}
+ down={this._marqueeing} scaling={returnOne}
+ addDocument={this.addDocumentWrapper}
+ docView={this.props.docViewPath().lastElement()}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this._savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current} />}
+ </div>
+ {/* </LoadScript > */}
+ <div className="mapBox-sidebar"
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ <div className="mapBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? "none" : undefined,
+ top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK
+ }}
+ onPointerDown={this.sidebarBtnDown} >
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 30b4dc92a..d54b65d92 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -136,6 +136,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
+ // adding external documents; to sidebar key
+ // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 72e84c141..e61f96852 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -120,6 +120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public ProseRef?: HTMLDivElement;
public get EditorView() { return this._editorView; }
public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+ @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); };
@computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
@computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
@@ -1502,6 +1503,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ // console.log("printting allSideBarDocs");
+ // console.log(this.allSidebarDocs);
return this.addDocument(doc, sidebarKey);
}
sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
@@ -1520,6 +1523,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
const color = !annotated ? Colors.WHITE : Colors.BLACK;
const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+
return (!annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging())) ? (null) :
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
style={{
@@ -1541,6 +1545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ // usePanelWidth={true}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
showSidebar={this.SidebarShown}
PanelWidth={this.sidebarWidth}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2a153f256..14d6e8be6 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -46,6 +46,11 @@ const PresBoxDocument = makeInterface(documentSchema);
export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+ /**
+ * transitions & effects for documents
+ * @param renderDoc
+ * @param layoutDoc
+ */
static renderEffectsDoc(renderDoc: any, layoutDoc: Doc) {
const effectProps = {
left: layoutDoc.presEffectDirection === PresEffect.Left,
@@ -223,6 +228,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
+ //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
// No more frames in current doc and next slide is defined, therefore move to next slide
nextSlide = (activeNext: Doc) => {
const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
@@ -416,7 +422,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
/**
* Uses the viewfinder to progressivize through the different views of a single collection.
- * @param presTargetDoc: document for which internal zoom is used
+ * @param activeItem: document for which internal zoom is used
*/
zoomProgressivizeNext = (activeItem: Doc) => {
const targetDoc: Doc = this.targetDoc;
@@ -532,6 +538,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
load();
}
+ // The function pauses the auto presentation
@action
pauseAutoPres = () => {
if (this.layoutDoc.presStatus === PresStatus.Autoplay) {
@@ -555,6 +562,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
});
}
+ // The function allows for viewing the pres path on toggle
@action togglePath = (srcContext: Doc, off?: boolean) => {
if (off) {
this._pathBoolean = false;
@@ -565,6 +573,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
+ // The function allows for expanding the view of pres on toggle
@action toggleExpandMode = () => {
runInAction(() => this._expandBoolean = !this._expandBoolean);
this.rootDoc.expandBoolean = this._expandBoolean;
@@ -1844,6 +1853,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
case DocumentType.VID: type = "Video"; break;
case DocumentType.IMG: type = "Image"; break;
case DocumentType.WEB: type = "Web page"; break;
+ case DocumentType.MAP: type = "Map"; break;
default: type = "Other node"; break;
}
}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5e713c3cf..238d025dc 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -166,6 +166,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
e.preventDefault();
}
+ /**
+ * Function to drag and drop the pres element to a diferent location
+ */
startDrag = (e: PointerEvent) => {
const miniView: boolean = this.toolbarWidth <= 100;
const activeItem = this.rootDoc;
@@ -244,6 +247,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
e.stopPropagation();
});
+ // set the value/title of the individual pres element
@undoBatch
@action
onSetValue = (value: string) => {
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index f1dd106a7..6103b245c 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -14,7 +14,7 @@ export interface IconBarProps {
@observer
export class IconBar extends React.Component<IconBarProps> {
- public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB];
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP];
@observable private _icons: string[] = this._allIcons;
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
index 2dd6b1b79..6cf3a5f72 100644
--- a/src/client/views/search/IconButton.tsx
+++ b/src/client/views/search/IconButton.tsx
@@ -70,6 +70,7 @@ export class IconButton extends React.Component<IconButtonProps>{
case (DocumentType.RTF): return "sticky-note";
case (DocumentType.VID): return "video";
case (DocumentType.WEB): return "globe-asia";
+ case (DocumentType.MAP): return "map-marker-alt";
default: return "caret-down";
}
}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 42d93c60c..328385fda 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -22,7 +22,7 @@ import { RichTextField } from "./RichTextField";
import { listSpec } from "./Schema";
import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField";
+import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField";
import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
import JSZip = require("jszip");
@@ -638,6 +638,7 @@ export namespace Doc {
else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
+ else if (value instanceof MapField) return { url: value.url.href, __type: "map" };
else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
@@ -1240,6 +1241,7 @@ export namespace Doc {
case DocumentType.INK: return "pen-nib";
case DocumentType.PDF: return "file-pdf";
case DocumentType.LINK: return "link";
+ case DocumentType.MAP: return "map-marker-alt"
default: return "question";
}
}
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index d96e8a70a..4d3776a2c 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -57,6 +57,7 @@ export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
+@scriptingGlobal @Deserializable("map") export class MapField extends URLField { }
@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { }
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index db2c6ca5b..c35c52699 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -26,6 +26,8 @@ export const documentSchema = createSchema({
z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview
zIndex: "number", // zIndex of a document in a freeform view
_scrollTop: "number", // scroll position of a scrollable document (pdf, text, web)
+ lat: "number",
+ lng: "number",
// appearance properties on the layout
"_backgroundGrid-spacing": "number", // the size of the grid for collection views
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index 0d4472fdc..2175b6db6 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -112,7 +112,7 @@ async function getDocs(id: string) {
const pathname = new URL(urlString).pathname;
files.add(pathname);
}
- } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) {
const url = new URL(field.url);
const pathname = url.pathname;
files.add(pathname);
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 775e90520..a74e13a62 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -185,6 +185,7 @@ export namespace SolrManager {
"pdf": ["_t", "url"],
"audio": ["_t", "url"],
"web": ["_t", "url"],
+ "map": ["_t", "url"],
"date": ["_d", value => new Date(value.date).toISOString()],
"proxy": ["_i", "fieldId"],
"prefetch_proxy": ["_i", "fieldId"],
diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts
index 6cc8aa941..0745ea455 100644
--- a/src/server/DashSession/Session/agents/process_message_router.ts
+++ b/src/server/DashSession/Session/agents/process_message_router.ts
@@ -33,7 +33,7 @@ export default abstract class IPCMessageReceiver {
}
}
- /**
+ /**
* Unregister all listeners at this message.
*/
public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]);
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 7c441e3c0..c60880882 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -45,7 +45,7 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) {
}
exts.push(ext);
}
- } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) {
const url = new URL(field.url);
const pathname = url.pathname;
const ext = path.extname(pathname);
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
index 7178add93..e9f9da25a 100644
--- a/src/server/remapUrl.ts
+++ b/src/server/remapUrl.ts
@@ -8,7 +8,8 @@ const suffixMap: { [type: string]: true } = {
"pdf": true,
"audio": true,
"web": true,
- "image": true
+ "image": true,
+ "map": true,
};
async function update() {
diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts
index e9860bd61..c5552f6bf 100644
--- a/src/server/updateProtos.ts
+++ b/src/server/updateProtos.ts
@@ -1,7 +1,7 @@
import { Database } from "./database";
const protos =
- ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc"];
+ ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc", "map"];
(async function () {
await Promise.all(
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 224a9eefb..13d7237f6 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -229,6 +229,7 @@ export namespace WebSocket {
"pdf": ["_t", "url"],
"audio": ["_t", "url"],
"web": ["_t", "url"],
+ "map": ["_t", "url"],
"script": ["_t", value => value.script.originalScript],
"RichTextField": ["_t", value => value.Text],
"date": ["_d", value => new Date(value.date).toISOString()],