diff --git a/doc/customizing/skinning.md b/doc/customizing/skinning.md
index bbd1bdea5a..f3cedf4fb2 100644
--- a/doc/customizing/skinning.md
+++ b/doc/customizing/skinning.md
@@ -29,7 +29,7 @@ import React from 'react';
import version from '../../version';
-import StandardUserInterface from 'terriajs/lib/ReactViews/StandardUserInterface/StandardUserInterface.jsx';
+import { StandardUserInterface } from 'terriajs/lib/ReactViews/StandardUserInterface/StandardUserInterface.jsx';
import MenuItem from 'terriajs/lib/ReactViews/StandardUserInterface/customizable/MenuItem';
import RelatedMaps from './RelatedMaps';
import { Menu, Nav } from 'terriajs/lib/ReactViews/StandardUserInterface/customizable/Groups';
diff --git a/doc/deploying/deploying-terriamap.md b/doc/deploying/deploying-terriamap.md
index bd0c5b681e..78ba3bd24b 100644
--- a/doc/deploying/deploying-terriamap.md
+++ b/doc/deploying/deploying-terriamap.md
@@ -28,6 +28,8 @@ And on the server, change to the directory where you copied those files and dire
The server will start on port 3001. You can specify a different port by adding `--port 1234` to the command-line above.
+Ensure that you specify the `baseHref` path in your `devserverconfig.json` if you are serving your TerriaMap from a directory, and an `appBaseUrl` so a sitemap & other links can be generated.
+
It is usually a good idea to run another web server, such as [nginx](https://nginx.org/en/) or [Varnish](https://varnish-cache.org/) on port 80 and then reverse-proxy to the Node.js server, rather than running terriajs-server on port 80 directly. You will find a varnish VCL file with the TerriaMap source code in the [deploy/varnish directory](https://github.com/TerriaJS/TerriaMap/tree/master/deploy/varnish). In addition to acting as a reverse proxy for the Node.js server, the supplied Varnish configuration also caches requests to proxied map data in order to improve performance.
### Using any web server
@@ -38,6 +40,7 @@ It is usually a good idea to run another web server, such as [nginx](https://ngi
2. It includes a simple service at `/proxy` that allows TerriaJS to access geospatial data servers that don't support [CORS](../connecting-to-data/cross-origin-resource-sharing.md). If this service is not available, TerriaJS won't be able to access any datasets that are on other servers and that don't support CORS.
3. It includes another service at `/convert` that uses [GDAL](http://www.gdal.org/) and OGR to transform otherwise unsupported geospatial vector data (e.g. shapefiles) to GeoJSON for display by the TerriaJS client. If this service is not available, these data formats will not be supported. However, all the [formats that TerriaJS supports directly](../connecting-to-data/catalog-items.md) will work just fine.
* When configured correctly, it persists blobs of JSON for use in the sharing feature. If this service is not available, the JSON can be stored in the share URL, instead. However, this makes for some extremely long URLs.
+4. It reroutes and serves up a build-time-prerendered index.html of your catalog URLs so they can be indexed by search engines.
If these limitations are acceptable, you can run your TerriaMap on virtually any web server by simply copying the TerriaMap `wwwroot` onto the server!
diff --git a/doc/deploying/prerender-troubleshooting.md b/doc/deploying/prerender-troubleshooting.md
new file mode 100644
index 0000000000..cf39f9c1e8
--- /dev/null
+++ b/doc/deploying/prerender-troubleshooting.md
@@ -0,0 +1,16 @@
+# Prerender Troubleshooting
+TerriaMap prerenders pages through puppeteer.
+It should JustWork on MacOS. Linux will require some extra deps. Some more general troubleshooting at:
+https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md
+
+## WSL
+You'll need to tell PrerenderSPAPlugin to pass an `executablePath` down to puppeteer.
+
+via @steve9164's findings
+
+Add (changing the path to where your chrome is)
+`executablePath: '/mnt/c/Program\ Files\ \(x86\)/Google/Chrome/Application/chrome.exe'`
+
+to
+
+https://github.com/TerriaJS/TerriaMap/blob/c8675bc62cd5e37b6df490910f6ed7f7ae264d95/buildprocess/webpack.config.js#L155
diff --git a/lib/Core/ConsoleAnalytics.js b/lib/Core/ConsoleAnalytics.js
index 80a2b5d752..e09add2074 100644
--- a/lib/Core/ConsoleAnalytics.js
+++ b/lib/Core/ConsoleAnalytics.js
@@ -28,4 +28,10 @@ ConsoleAnalytics.prototype.logEvent = function(category, action, label, value) {
}
};
+ConsoleAnalytics.prototype.logPageView = function(location) {
+ if (this.logToConsole) {
+ console.log("New pageview at location: ", location);
+ }
+};
+
module.exports = ConsoleAnalytics;
diff --git a/lib/Core/GoogleAnalytics.js b/lib/Core/GoogleAnalytics.js
index 37eaaf33cd..29a86162d5 100644
--- a/lib/Core/GoogleAnalytics.js
+++ b/lib/Core/GoogleAnalytics.js
@@ -20,6 +20,12 @@ GoogleAnalytics.prototype.logEvent = function(category, action, label, value) {
ga("send", "event", category, action, label, value);
};
+GoogleAnalytics.prototype.logPageView = function(location) {
+ initializeGoogleAnalytics(this);
+ ga("set", "page", location);
+ ga("send", "pageview");
+};
+
function initializeGoogleAnalytics(that) {
if (defined(window.ga)) {
return;
diff --git a/lib/Models/Terria.js b/lib/Models/Terria.js
index 1171def07c..b9255ca250 100644
--- a/lib/Models/Terria.js
+++ b/lib/Models/Terria.js
@@ -47,6 +47,7 @@ var defaultConfigParameters = {
defaultMaximumShownFeatureInfos: 100,
/* These services are not included within Terria, but this is where we expect them to be, by default. */
regionMappingDefinitionsUrl: "build/TerriaJS/data/regionMapping.json",
+ appBaseUrl: undefined,
conversionServiceBaseUrl: "convert/",
proj4ServiceBaseUrl: "proj4/",
corsProxyBaseUrl: "proxy/",
@@ -524,6 +525,7 @@ Terria.prototype.start = function(options) {
that.configParameters = combine(config.parameters, that.configParameters);
}
var cp = that.configParameters;
+ cp.appBaseUrl = slashify(cp.appBaseUrl);
cp.conversionServiceBaseUrl = slashify(cp.conversionServiceBaseUrl);
cp.proj4ServiceBaseUrl = slashify(cp.proj4ServiceBaseUrl);
cp.corsProxyBaseUrl = slashify(cp.corsProxyBaseUrl);
diff --git a/lib/ReactViewModels/TerriaRouting.js b/lib/ReactViewModels/TerriaRouting.js
new file mode 100644
index 0000000000..4b13255505
--- /dev/null
+++ b/lib/ReactViewModels/TerriaRouting.js
@@ -0,0 +1,14 @@
+// Route paths for the app.
+
+var ROOT_ROUTE = "/";
+
+var CATALOG_ROUTE = `${ROOT_ROUTE}catalog/`;
+// var CATALOG_ROUTE = "/catalog/";
+
+var CATALOG_MEMBER_ROUTE = `${CATALOG_ROUTE}:catalogMemberId`;
+
+module.exports = {
+ ROOT_ROUTE,
+ CATALOG_ROUTE,
+ CATALOG_MEMBER_ROUTE
+};
diff --git a/lib/ReactViewModels/ViewState.js b/lib/ReactViewModels/ViewState.js
index fdeaa20287..8b9e4ed565 100644
--- a/lib/ReactViewModels/ViewState.js
+++ b/lib/ReactViewModels/ViewState.js
@@ -6,6 +6,8 @@ import addedByUser from "../Core/addedByUser";
import knockout from "terriajs-cesium/Source/ThirdParty/knockout";
import MouseCoords from "./MouseCoords";
import SearchState from "./SearchState";
+import URI from "urijs";
+
import "../Models/i18n.js";
export const DATA_CATALOG_NAME = "data-catalog";
export const USER_DATA_NAME = "my-data";
@@ -14,6 +16,9 @@ import { analyticsSetShowGuide } from "../ReactViews/Guide/Guide";
import { SATELLITE_GUIDE_KEY } from "../ReactViews/Guide/SatelliteGuide";
import { LOCAL_PROPERTY_KEY as WELCOME_PROPERTY_KEY } from "../ReactViews/WelcomeMessage/WelcomeMessage";
+// import { ROOT_ROUTE, CATALOG_ROUTE } from "../ReactViewModels/TerriaRouting.js";
+import { ROOT_ROUTE } from "../ReactViewModels/TerriaRouting.js";
+
/**
* Root of a global view model. Presumably this should get nested as more stuff goes into it. Basically this belongs to
* the root of the UI and then it can choose to pass either the whole thing or parts down as props to its children.
@@ -39,7 +44,6 @@ export default class ViewState {
this.terria = terria;
this.previewedItem = undefined;
this.userDataPreviewedItem = undefined;
- this.explorerPanelIsVisible = false;
this.shareModalIsVisible = false;
this.activeTabCategory = DATA_CATALOG_NAME;
this.activeTabSubCategory = null; // Used to refer to an individual data-catalog tab
@@ -47,6 +51,8 @@ export default class ViewState {
this.mobileView = null;
this.isMapFullScreen = false;
this.myDataIsUploadView = true;
+ this.matchFromExplorer = {};
+ this.history = {};
/**
* Gets or sets a value indicating whether the small screen (mobile) user interface should be used.
@@ -107,6 +113,7 @@ export default class ViewState {
this.featurePrompts = [];
+ this._explorerPanelIsVisible = false;
/**
* The tool that will appear in the tool panel.
*/
@@ -143,9 +150,10 @@ export default class ViewState {
});
knockout.track(this, [
+ "matchFromExplorer",
"previewedItem",
"catalogSearch",
- "explorerPanelIsVisible",
+ "_explorerPanelIsVisible",
"shareModalIsVisible",
"activeTabCategory",
"activeTabIdInCategory",
@@ -174,6 +182,21 @@ export default class ViewState {
"_showHelpMenu"
]);
+ knockout.defineProperty(this, "explorerPanelIsVisible", {
+ get: function() {
+ return this._explorerPanelIsVisible;
+ },
+ set: function(bool) {
+ if (!bool && this.location && this.location.pathname !== ROOT_ROUTE) {
+ setTimeout(() => {
+ this.history.push(ROOT_ROUTE);
+ this._explorerPanelIsVisible = false;
+ }, 300);
+ }
+ this._explorerPanelIsVisible = bool;
+ }
+ });
+
knockout.defineProperty(this, "chartIsOpen", {
get: function() {
const chartableItems = this.terria.catalog.chartableItems;
@@ -214,7 +237,6 @@ export default class ViewState {
});
// Reflect preview id from terria when loaded from an init source
-
this._sharedFromExplorerPanelSubscription = knockout
.getObservable(terria, "sharedFromExplorerPanel")
.subscribe(sharedFromExplorerPanel => {
@@ -292,6 +314,24 @@ export default class ViewState {
this._disclaimerHandler = new DisclaimerHandler(terria, this);
+ this._matchFromExplorerSubscription = knockout
+ .getObservable(this, "matchFromExplorer")
+ .subscribe(matchFromExplorer => {
+ if (
+ matchFromExplorer &&
+ matchFromExplorer.params &&
+ matchFromExplorer.params.catalogMemberId
+ ) {
+ // viewCatalogMember handles which tab to activate
+ const idToFind = URI.decode(matchFromExplorer.params.catalogMemberId);
+ const catalogMember = terria.catalog.shareKeyIndex[idToFind];
+
+ if (defined(catalogMember)) {
+ this.viewCatalogMember(catalogMember);
+ }
+ }
+ });
+
this._storyPromptHandler = knockout
.getObservable(this, "storyShown")
.subscribe(storyShown => {
@@ -323,6 +363,7 @@ export default class ViewState {
}
dispose() {
+ this._matchFromExplorerSubscription.dispose();
this._sharedFromExplorerPanelSubscription.dispose();
this._previewedItemIdSubscription.dispose();
this._pickedFeaturesSubscription.dispose();
@@ -335,13 +376,26 @@ export default class ViewState {
this._shouldStartSatelliteGuidanceSubscription.dispose();
}
+ checkCatalogRoute() {
+ // buggy with current implementation of mobile-catalog logic
+ // if (defined(this.location) && defined(this.history)) {
+ // if (this.location.pathname === ROOT_ROUTE) {
+ // this.history.push(CATALOG_ROUTE);
+ // // would have done this if it syncs up with the mobile catalog, leaving commented out for now
+ // // this.explorerPanelIsVisible = true;
+ // }
+ // }
+ }
+
openAddData() {
+ this.checkCatalogRoute();
this.explorerPanelIsVisible = true;
this.activeTabCategory = DATA_CATALOG_NAME;
this.switchMobileView(this.mobileViewOptions.data);
}
openUserData() {
+ this.checkCatalogRoute();
this.explorerPanelIsVisible = true;
this.activeTabCategory = USER_DATA_NAME;
}
diff --git a/lib/ReactViews/DataCatalog/CatalogGroup.jsx b/lib/ReactViews/DataCatalog/CatalogGroup.jsx
index a3d69ac57b..1133d1e6fc 100644
--- a/lib/ReactViews/DataCatalog/CatalogGroup.jsx
+++ b/lib/ReactViews/DataCatalog/CatalogGroup.jsx
@@ -1,4 +1,5 @@
import React from "react";
+import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import classNames from "classnames";
@@ -17,8 +18,8 @@ function CatalogGroup(props) {
const { t } = useTranslation();
return (