diff --git a/app/plugins/plugin_base.py b/app/plugins/plugin_base.py index 02c75659..de562a3e 100644 --- a/app/plugins/plugin_base.py +++ b/app/plugins/plugin_base.py @@ -1,5 +1,5 @@ import logging, os, sys -from abc import ABC, abstractmethod +from abc import ABC logger = logging.getLogger('app.logger') @@ -7,7 +7,6 @@ class PluginBase(ABC): def __init__(self): self.name = self.get_module_name().split(".")[-2] - @abstractmethod def register(self): pass @@ -27,6 +26,9 @@ class PluginBase(ABC): def get_include_js_urls(self): return ["/plugins/{}/{}".format(self.get_name(), js_file) for js_file in self.include_js_files()] + def get_include_css_urls(self): + return ["/plugins/{}/{}".format(self.get_name(), css_file) for css_file in self.include_css_files()] + def has_public_path(self): return os.path.isdir(self.get_path("public")) @@ -38,5 +40,13 @@ class PluginBase(ABC): """ return [] + def include_css_files(self): + """ + Should be overriden by plugins to communicate + which CSS files should be included in the WebODM interface + All paths are relative to a plugin's /public folder. + """ + return [] + def __str__(self): return "[{}]".format(self.get_module_name()) \ No newline at end of file diff --git a/app/static/app/css/theme.scss b/app/static/app/css/theme.scss index dfcec22c..b9c7b148 100644 --- a/app/static/app/css/theme.scss +++ b/app/static/app/css/theme.scss @@ -9,6 +9,9 @@ ul#side-menu.nav a, { color: theme("primary"); } +.theme-border-primary{ + border-color: theme("primary"); +} .tooltip{ .tooltip-inner{ background-color: theme("primary"); @@ -162,6 +165,9 @@ footer, .popover-title{ border-bottom-color: theme("border"); } +.theme-border{ + border-color: theme("border"); +} /* Highlight */ .task-list-item:nth-child(odd), diff --git a/app/static/app/js/classes/PluginsAPI.js b/app/static/app/js/classes/PluginsAPI.js deleted file mode 100644 index ab1e5375..00000000 --- a/app/static/app/js/classes/PluginsAPI.js +++ /dev/null @@ -1,26 +0,0 @@ -import { EventEmitter } from 'fbemitter'; -import Utils from './Utils'; - -const { assert } = Utils; - -if (!window.PluginsAPI){ - const events = new EventEmitter(); - - window.PluginsAPI = { - Map: { - AddPanel: (callback) => { - events.addListener('Map::Loaded', callback); - }, - - Loaded: (params) => { - assert(params.map !== undefined); - events.emit('Map::Loaded', params); - } - }, - - events - }; -} - -export default window.PluginsAPI; - diff --git a/app/static/app/js/classes/Utils.js b/app/static/app/js/classes/Utils.js index 4e4c528b..5f429602 100644 --- a/app/static/app/js/classes/Utils.js +++ b/app/static/app/js/classes/Utils.js @@ -73,6 +73,13 @@ export default { } throw message; // Fallback } + }, + + getCurrentScriptDir: function(){ + let scripts= document.getElementsByTagName('script'); + let path= scripts[scripts.length-1].src.split('?')[0]; // remove any ?query + let mydir= path.split('/').slice(0, -1).join('/')+'/'; // remove last filename part of path + return mydir; } }; diff --git a/app/static/app/js/classes/plugins/API.js b/app/static/app/js/classes/plugins/API.js new file mode 100644 index 00000000..c604cc82 --- /dev/null +++ b/app/static/app/js/classes/plugins/API.js @@ -0,0 +1,30 @@ +import { EventEmitter } from 'fbemitter'; +import ApiFactory from './ApiFactory'; +import Map from './Map'; +import $ from 'jquery'; +import SystemJS from 'SystemJS'; + +if (!window.PluginsAPI){ + const events = new EventEmitter(); + const factory = new ApiFactory(events); + + SystemJS.config({ + baseURL: '/plugins', + map: { + css: '/static/app/js/vendor/css.js' + }, + meta: { + '*.css': { loader: 'css' } + } + }); + + window.PluginsAPI = { + Map: factory.create(Map), + + SystemJS, + events + }; +} + +export default window.PluginsAPI; + diff --git a/app/static/app/js/classes/plugins/ApiFactory.js b/app/static/app/js/classes/plugins/ApiFactory.js new file mode 100644 index 00000000..03205c68 --- /dev/null +++ b/app/static/app/js/classes/plugins/ApiFactory.js @@ -0,0 +1,53 @@ +import SystemJS from 'SystemJS'; + +export default class ApiFactory{ + // @param events {EventEmitter} + constructor(events){ + this.events = events; + } + + // @param api {Object} + create(api){ + + // Adds two functions to obj + // - eventName + // - triggerEventName + // We could just use events, but methods + // are more robust as we can detect more easily if + // things break + const addEndpoint = (obj, eventName, preTrigger = () => {}) => { + obj[eventName] = (callbackOrDeps, callbackOrUndef) => { + if (Array.isArray(callbackOrDeps)){ + // Deps + // Load dependencies, then raise event as usual + // by appending the dependencies to the argument list + this.events.addListener(`${api.namespace}::${eventName}`, (...args) => { + Promise.all(callbackOrDeps.map(dep => SystemJS.import(dep))) + .then((...deps) => { + callbackOrUndef(...(Array.from(args).concat(...deps))); + }); + }); + }else{ + // Callback + this.events.addListener(`${api.namespace}::${eventName}`, callbackOrDeps); + } + } + + const triggerEventName = "trigger" + eventName[0].toUpperCase() + eventName.slice(1); + + obj[triggerEventName] = (...args) => { + preTrigger(...args); + this.events.emit(`${api.namespace}::${eventName}`, ...args); + }; + } + + const obj = {}; + api.endpoints.forEach(endpoint => { + if (!Array.isArray(endpoint)) endpoint = [endpoint]; + addEndpoint(obj, ...endpoint); + }); + return obj; + } + +} + diff --git a/app/static/app/js/classes/plugins/Map.js b/app/static/app/js/classes/plugins/Map.js new file mode 100644 index 00000000..368c861d --- /dev/null +++ b/app/static/app/js/classes/plugins/Map.js @@ -0,0 +1,17 @@ +import Utils from '../Utils'; + +const { assert } = Utils; + +const leafletPreCheck = (options) => { + assert(options.map !== undefined); +}; + +export default { + namespace: "Map", + + endpoints: [ + ["willAddControls", leafletPreCheck], + ["didAddControls", leafletPreCheck] + ] +}; + diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 11f47ca7..9d96478f 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -3,8 +3,6 @@ import '../css/Map.scss'; import 'leaflet/dist/leaflet.css'; import Leaflet from 'leaflet'; import async from 'async'; -import 'leaflet-measure/dist/leaflet-measure.css'; -import 'leaflet-measure/dist/leaflet-measure'; import '../vendor/leaflet/L.Control.MousePosition.css'; import '../vendor/leaflet/L.Control.MousePosition'; import '../vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css'; @@ -15,7 +13,7 @@ import SwitchModeButton from './SwitchModeButton'; import ShareButton from './ShareButton'; import AssetDownloads from '../classes/AssetDownloads'; import PropTypes from 'prop-types'; -import PluginsAPI from '../classes/PluginsAPI'; +import PluginsAPI from '../classes/plugins/API'; class Map extends React.Component { static defaultProps = { @@ -173,16 +171,22 @@ class Map extends React.Component { this.map = Leaflet.map(this.container, { scrollWheelZoom: true, - positionControl: true + positionControl: true, + zoomControl: false }); - const measureControl = Leaflet.control.measure({ - primaryLengthUnit: 'meters', - secondaryLengthUnit: 'feet', - primaryAreaUnit: 'sqmeters', - secondaryAreaUnit: 'acres' + PluginsAPI.Map.triggerWillAddControls({ + map: this.map }); - measureControl.addTo(this.map); + + Leaflet.control.scale({ + maxWidth: 250, + }).addTo(this.map); + + //add zoom control with your options + Leaflet.control.zoom({ + position:'bottomleft' + }).addTo(this.map); if (showBackground) { this.basemaps = { @@ -215,10 +219,6 @@ class Map extends React.Component { }).addTo(this.map); this.map.fitWorld(); - - Leaflet.control.scale({ - maxWidth: 250, - }).addTo(this.map); this.map.attributionControl.setPrefix(""); this.loadImageryLayers(true).then(() => { @@ -239,7 +239,7 @@ class Map extends React.Component { // PluginsAPI.events.addListener('Map::AddPanel', (e) => { // console.log("Received response: " + e); // }); - PluginsAPI.Map.Loaded({ + PluginsAPI.Map.triggerDidAddControls({ map: this.map }); } @@ -275,6 +275,7 @@ class Map extends React.Component { return (
+
(this.container = domNode)} diff --git a/app/static/app/js/css/Map.scss b/app/static/app/js/css/Map.scss index 94bf62d0..ae2b1758 100644 --- a/app/static/app/js/css/Map.scss +++ b/app/static/app/js/css/Map.scss @@ -1,4 +1,6 @@ .map{ + position: relative; + .leaflet-popup-content{ .title{ font-weight: bold; @@ -24,7 +26,7 @@ .shareButton{ z-index: 2000; - bottom: -11px; + bottom: 11px; right: 38px; } } \ No newline at end of file diff --git a/app/static/app/js/css/SharePopup.scss b/app/static/app/js/css/SharePopup.scss index 5b154eac..6b018e97 100644 --- a/app/static/app/js/css/SharePopup.scss +++ b/app/static/app/js/css/SharePopup.scss @@ -3,7 +3,7 @@ display: block; &.top{ - top: -32px; + top: -54px; } &.bottom{ top: 32px; diff --git a/app/static/app/js/css/SwitchModeButton.scss b/app/static/app/js/css/SwitchModeButton.scss index 0d636537..8ba9c131 100644 --- a/app/static/app/js/css/SwitchModeButton.scss +++ b/app/static/app/js/css/SwitchModeButton.scss @@ -2,6 +2,6 @@ border-width: 1px; position: absolute; z-index: 2000; - bottom: -22px; + bottom: 22px; right: 12px; } diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx index 143dca18..a867a094 100644 --- a/app/static/app/js/main.jsx +++ b/app/static/app/js/main.jsx @@ -1,7 +1,7 @@ import '../css/main.scss'; import './django/csrf'; import ReactDOM from 'react-dom'; -import PluginsAPI from './classes/PluginsAPI'; +import PluginsAPI from './classes/plugins/API'; // Main is always executed first in the page diff --git a/app/static/app/js/vendor/css-browser.js b/app/static/app/js/vendor/css-browser.js new file mode 100644 index 00000000..d7f1578d --- /dev/null +++ b/app/static/app/js/vendor/css-browser.js @@ -0,0 +1,89 @@ +var waitSeconds = 100; + + var head = document.getElementsByTagName('head')[0]; + + var isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/); + var webkitLoadCheck = function(link, callback) { + setTimeout(function() { + for (var i = 0; i < document.styleSheets.length; i++) { + var sheet = document.styleSheets[i]; + if (sheet.href == link.href) + return callback(); + } + webkitLoadCheck(link, callback); + }, 10); + }; + + var cssIsReloadable = function cssIsReloadable(links) { + // Css loaded on the page initially should be skipped by the first + // systemjs load, and marked for reload + var reloadable = true; + forEach(links, function(link) { + if(!link.hasAttribute('data-systemjs-css')) { + reloadable = false; + link.setAttribute('data-systemjs-css', ''); + } + }); + return reloadable; + } + + var findExistingCSS = function findExistingCSS(url){ + // Search for existing link to reload + var links = head.getElementsByTagName('link') + return filter(links, function(link){ return link.href === url; }); + } + + var noop = function() {}; + + var loadCSS = function(url, existingLinks) { + return new Promise(function(resolve, reject) { + var timeout = setTimeout(function() { + reject('Unable to load CSS'); + }, waitSeconds * 1000); + var _callback = function(error) { + clearTimeout(timeout); + link.onload = link.onerror = noop; + setTimeout(function() { + if (error) + reject(error); + else + resolve(''); + }, 7); + }; + var link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = url; + link.setAttribute('data-systemjs-css', ''); + if (!isWebkit) { + link.onload = function() { + _callback(); + } + } else { + webkitLoadCheck(link, _callback); + } + link.onerror = function(event) { + _callback(event.error || new Error('Error loading CSS file.')); + }; + if (existingLinks.length) + head.insertBefore(link, existingLinks[0]); + else + head.appendChild(link); + }) + // Remove the old link regardless of loading outcome + .then(function(result){ + forEach(existingLinks, function(link){link.parentElement.removeChild(link);}) + return result; + }, function(err){ + forEach(existingLinks, function(link){link.parentElement.removeChild(link);}) + throw err; + }) + }; + + exports.fetch = function(load) { + // dont reload styles loaded in the head + var links = findExistingCSS(load.address); + if(!cssIsReloadable(links)) + return ''; + return loadCSS(load.address, links); + }; \ No newline at end of file diff --git a/app/static/app/js/vendor/css-plugin-base.js b/app/static/app/js/vendor/css-plugin-base.js new file mode 100644 index 00000000..3d54c8fa --- /dev/null +++ b/app/static/app/js/vendor/css-plugin-base.js @@ -0,0 +1,70 @@ +/* + * Base CSS Plugin Class + */ + +function CSSPluginBase(compileCSS) { + this.compileCSS = compileCSS; + + this.translate = function(load, opts) { + var loader = this; + if (loader.builder && loader.buildCSS === false) { + load.metadata.build = false; + return; + } + + var path = this._nodeRequire && this._nodeRequire('path'); + + return Promise.resolve(compileCSS.call(loader, load.source, load.address, load.metadata.loaderOptions || {})) + .then(function(result) { + load.metadata.style = result.css; + load.metadata.styleSourceMap = result.map; + if (result.moduleFormat) + load.metadata.format = result.moduleFormat; + return result.moduleSource || ''; + }); + }; +} + +var isWin = typeof process != 'undefined' && process.platform.match(/^win/); +function toFileURL(path) { + return 'file://' + (isWin ? '/' : '') + path.replace(/\\/g, '/'); +} + +var builderPromise; +function getBuilder(loader) { + if (builderPromise) + return builderPromise; + return builderPromise = loader['import']('./css-plugin-base-builder.js', module.id); +} + +CSSPluginBase.prototype.bundle = function(loads, compileOpts, outputOpts) { + var loader = this; + return getBuilder(loader) + .then(function(builder) { + return builder.bundle.call(loader, loads, compileOpts, outputOpts); + }); +}; + +CSSPluginBase.prototype.listAssets = function(loads, opts) { + var loader = this; + return getBuilder(loader) + .then(function(builder) { + return builder.listAssets.call(loader, loads, opts); + }); +}; + +/* + *