kopia lustrzana https://github.com/FacilMap/facilmap
Render table as static html under fixed URL (fixes #58)
rodzic
68e2cbdad1
commit
79588d05e1
|
@ -39,11 +39,14 @@
|
|||
<ul uib-dropdown-menu aria-labelledby="toolbox-layers-dropdown" class="dropdown-menu-right">
|
||||
<!--<li ng-if="!readonly"><a href="javascript:" ng-click="openDialog('copy-pad-dialog')">Copy pad</a></li>-->
|
||||
<li><a href="javascript:" ng-click="importFile()">Import file</a></li>
|
||||
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="showHistory()">Show edit history</a></li>
|
||||
<li ng-if="padId"><a href="{{padData.id}}/gpx?useTracks=1">Export as GPX (tracks)</a></li>
|
||||
<li ng-if="padId"><a href="{{padData.id}}/gpx?useTracks=0">Export as GPX (routes)</a></li>
|
||||
<li ng-if="padId"><a href="{{padData.id}}/table" target="_blank">Export as table</a></li>
|
||||
<li ng-if="padId" role="separator" class="divider"></li>
|
||||
<li ng-if="padId"><a href="javascript:" ng-click="filter()">Filter</a></li>
|
||||
<li ng-if="padId"><a href="{{padId}}/gpx?useTracks=1">Export GPX</a></li>
|
||||
<li ng-if="padId"><a href="javascript:" ng-click="showTable()">View as table</a></li>
|
||||
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="editPadSettings()">Settings</a></li>
|
||||
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="showHistory()">Show edit history</a></li>
|
||||
<li ng-if="padId" role="separator" class="divider"></li>
|
||||
<li><a href="javascript:" ng-click="showAbout()">About FacilMap</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fm from '../../app';
|
||||
import $ from 'jquery';
|
||||
|
||||
fm.app.factory("fmMapToolbox", function($compile, fmTable, fmFilter) {
|
||||
fm.app.factory("fmMapToolbox", function($compile, fmFilter) {
|
||||
return function(map) {
|
||||
var scope = map.socket.$new();
|
||||
|
||||
|
@ -32,10 +32,6 @@ fm.app.factory("fmMapToolbox", function($compile, fmTable, fmFilter) {
|
|||
|
||||
scope.editObjectTypes = map.typesUi.editTypes.bind(map.typesUi);
|
||||
|
||||
scope.showTable = function() {
|
||||
fmTable.showTable(map.socket.padId);
|
||||
};
|
||||
|
||||
scope.importFile = function() {
|
||||
map.importUi.openImportDialog();
|
||||
};
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
table th.sort-none,table th.sort-up {
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
table th.sort-down {
|
||||
cursor: n-resize;
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="modal-title">Table</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h2 ng-repeat-start="type in types">{{type.name}}</h2>
|
||||
<table ng-if="type.type == 'marker'" class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort" ng-click="sort(type, '__name')" ng-class="getSortClass(type, '__name')">Name <span ng-class="getSortIcon(type, '__name')"></span></th>
|
||||
<th>Position</th>
|
||||
<th class="sort" ng-repeat="field in type.fields" ng-click="sort(type, field.name)" ng-class="getSortClass(type, field.name)">{{field.name}} <span ng-class="getSortIcon(type, field.name)"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="marker in markers | fmType:type.id | orderBy : getSortField(type) : sortOrder[type.id]">
|
||||
<td>{{marker.name}}</td>
|
||||
<td>{{marker.lat}},{{marker.lon}}</td>
|
||||
<td ng-repeat="field in type.fields" fm-type-field-content="field" fm-type-field-model="marker.data[field.name]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table ng-repeat-end ng-if="type.type == 'line'" class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort" ng-click="sort(type, '__name')" ng-class="getSortClass(type, '__name')">Name <span ng-class="getSortIcon(type, '__name')"></span></th>
|
||||
<th class="sort" ng-click="sort(type, '__distance')" ng-class="getSortClass(type, '__distance')">Distance <span ng-class="getSortIcon(type, '__distance')"></span></th>
|
||||
<th class="sort" ng-click="sort(type, '__time')" ng-class="getSortClass(type, '__time')">Time <span ng-class="getSortIcon(type, '__time')"></span></th>
|
||||
<th class="sort" ng-repeat="field in type.fields" ng-class="getSortClass(type, field.name)" ng-click="sort(type, field.name)">{{field.name}} <span ng-class="getSortIcon(type, field.name)"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="line in lines | fmType:type.id | orderBy : getSortField(type) : sortOrder[type.id]">
|
||||
<td>{{line.name}}</td>
|
||||
<td>{{line.distance | fmRound:2}} km</td>
|
||||
<td><span ng-show="line.time != null">{{line.time | fmFormatTime}} h {{line.mode | fmRoutingMode}}</span></td>
|
||||
<td ng-repeat="field in type.fields" fm-type-field-content="field" fm-type-field-model="line.data[field.name]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="$dismiss()">Close</button>
|
||||
</div>
|
|
@ -1,79 +0,0 @@
|
|||
import fm from '../app';
|
||||
import $ from 'jquery';
|
||||
|
||||
fm.app.filter("fmType", function() {
|
||||
return function(input, typeId) {
|
||||
var res = [ ];
|
||||
angular.forEach(input, function(it) {
|
||||
if(it.typeId == typeId)
|
||||
res.push(it);
|
||||
});
|
||||
return res;
|
||||
};
|
||||
});
|
||||
|
||||
fm.app.factory("fmTable", function(fmSocket, $rootScope, $uibModal) {
|
||||
return {
|
||||
showTable : function(padId) {
|
||||
var socket = fmSocket(padId);
|
||||
socket.updateBbox({ top: 90, left: -180, right: 180, bottom: -90, zoom: 0 });
|
||||
|
||||
$uibModal.open({
|
||||
template: require("./table.html"),
|
||||
scope: socket,
|
||||
controller: "fmTableCtrl",
|
||||
size: "fs"
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
fm.app.controller("fmTableCtrl", function($scope, fmTypeFields) {
|
||||
function _getField(type, fieldName) {
|
||||
for(var i=0; i<type.fields.length; i++) {
|
||||
if(type.fields[i].name == fieldName)
|
||||
return type.fields[i];
|
||||
}
|
||||
}
|
||||
|
||||
function _normaliseNumbers(text) { // Pads any number in the string with zeros, so that numbers have a unified length and can be string-sorted
|
||||
return (text ? ""+text : "").trim().toLowerCase().replace(/\d+/g, function(m) { return ("000000000"+m).slice(-10) });
|
||||
}
|
||||
|
||||
$scope.sort = function(type, field) {
|
||||
$scope.sortOrder[type.id] = (($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == field ? !$scope.sortOrder[type.id] : false);
|
||||
$scope.sortField[type.id] = field;
|
||||
};
|
||||
|
||||
$scope.getSortField = function(type) {
|
||||
var f = $scope.sortField[type.id];
|
||||
|
||||
if(f == null || f == "__name" || f == "__distance" || f == "__time")
|
||||
return function(it) { return _normaliseNumbers(it[f ? f.replace(/^__/, "") : "name"]) };
|
||||
else
|
||||
return function(it) { return _normaliseNumbers($("<div/>").append(fmTypeFields.formatField(_getField(type, f), it.data[f])).text()); };
|
||||
};
|
||||
|
||||
$scope.getSortClass = function(type, fieldName) {
|
||||
if(($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == fieldName) {
|
||||
return $scope.sortOrder[type.id] ? "sort-up" : "sort-down";
|
||||
} else {
|
||||
return "sort-none";
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getSortIcon = function(type, fieldName) {
|
||||
if(($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == fieldName) {
|
||||
return {
|
||||
'glyphicon': true,
|
||||
'glyphicon-triangle-bottom': !$scope.sortOrder[type.id],
|
||||
'glyphicon-triangle-top': $scope.sortOrder[type.id]
|
||||
};
|
||||
} else {
|
||||
return { };
|
||||
}
|
||||
};
|
||||
|
||||
$scope.sortField = { };
|
||||
$scope.sortOrder = { };
|
||||
});
|
|
@ -74,7 +74,7 @@ gulp.task("webpack", [ "icons" ], function() {
|
|||
return Promise.denodeify(fs.unlink)(staticFrontendFile);
|
||||
}).then(() => {
|
||||
// Create symlink with fixed file name so that people can include https://facilmap.org/frontend.js
|
||||
return Promise.denodeify(fs.symlink)(`frontend-${stats.hash}.js`, `${__dirname}/build/frontend.js`);
|
||||
return Promise.denodeify(fs.symlink)(`frontend-index-${stats.hash}.js`, `${__dirname}/build/frontend.js`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,7 +46,8 @@
|
|||
"leaflet.markercluster": "^1.0.3",
|
||||
"linkifyjs": "^2.1.3",
|
||||
"marked": "^0.3.6",
|
||||
"osmtogeojson": "^2.2.12"
|
||||
"osmtogeojson": "^2.2.12",
|
||||
"tablesorter": "^2.28.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.21.0",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
h2[aria-expanded=true] .glyphicon-chevron-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
table.collapse.in {
|
||||
display: table;
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%=padData.name%> – FacilMap</title>
|
||||
<base href="../" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<%
|
||||
if(!padData || padData.searchEngines) {
|
||||
%>
|
||||
<meta name="robots" content="index,nofollow" />
|
||||
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
|
||||
<%
|
||||
} else {
|
||||
%>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<%
|
||||
}
|
||||
%>
|
||||
<link rel="shortcut icon" href="../static/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<h1><%=padData.name%> – FacilMap</h1>
|
||||
<%
|
||||
for(let type of Object.values(types)) {
|
||||
%>
|
||||
<h2 role="button" data-toggle="collapse" data-target="#type-<%=type.id%>" aria-expanded="true" aria-controls="type-<%=type.id%>"><small><span class="glyphicon glyphicon-chevron-right hidden-print"></span></small> <%=type.name%></h2>
|
||||
<table id="type-<%=type.id%>" class="collapse in table table-striped table-bordered table-condensed tablesorter" data-sortlist="[[0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<%
|
||||
if(type.type == "marker") {
|
||||
%>
|
||||
<th data-sorter="false">Position</th>
|
||||
<%
|
||||
} else {
|
||||
%>
|
||||
<th>Distance</th>
|
||||
<th>Time</th>
|
||||
<%
|
||||
}
|
||||
|
||||
for(let field of type.fields) {
|
||||
%>
|
||||
<th><%=field.name%></th>
|
||||
<%
|
||||
}
|
||||
%>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%
|
||||
for(let object of type.type == "marker" ? type.markers : type.lines) {
|
||||
%>
|
||||
<tr>
|
||||
<td><%=object.name%></td>
|
||||
<%
|
||||
if(type.type == "marker") {
|
||||
%>
|
||||
<td><%=object.lat%>,<%=object.lon%></td>
|
||||
<%
|
||||
} else {
|
||||
%>
|
||||
<td><%=format.round(object.distance, 2)%> km</td>
|
||||
<td><% if(object.time != null) { %><%=format.formatTime(object.time)%> h <%=format.routingMode(object.mode)%><% } %></td>
|
||||
<%
|
||||
}
|
||||
|
||||
for(let field of type.fields) {
|
||||
%>
|
||||
<td><%-format.formatField(field, object.data[field.name])%></td>
|
||||
<%
|
||||
}
|
||||
%>
|
||||
</tr>
|
||||
<%
|
||||
}
|
||||
%>
|
||||
</tbody>
|
||||
</table>
|
||||
<%
|
||||
}
|
||||
%>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,27 @@
|
|||
import 'bootstrap';
|
||||
import $ from 'jquery';
|
||||
import 'tablesorter/dist/js/jquery.tablesorter';
|
||||
import 'tablesorter/dist/js/widgets/widget-uitheme.min.js';
|
||||
import 'tablesorter/dist/css/theme.bootstrap_3.min.css';
|
||||
import './table.css';
|
||||
|
||||
// Dereferrer
|
||||
$(document).on("click", "a", function(e) {
|
||||
var el = $(this);
|
||||
var href = el.attr("href");
|
||||
if(href && href.match(/^\s*(https?:)?\/\//i)) {
|
||||
el.attr("href", "deref.html?"+encodeURIComponent(href));
|
||||
|
||||
setTimeout(function() {
|
||||
el.attr("href", href);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
$("table.tablesorter").tablesorter({
|
||||
theme: "bootstrap",
|
||||
headerTemplate: "{content} {icon}",
|
||||
widgets: ["uitheme"]
|
||||
});
|
||||
});
|
|
@ -34,9 +34,12 @@ for(let i in addDeps) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
entry: __dirname + "/index/index.js",
|
||||
entry: {
|
||||
index: __dirname + "/index/index.js",
|
||||
table: __dirname + "/table/table.js"
|
||||
},
|
||||
output: {
|
||||
filename: "frontend-[hash].js",
|
||||
filename: "frontend-[name]-[hash].js",
|
||||
path: __dirname + "/build/"
|
||||
},
|
||||
resolve: {
|
||||
|
@ -72,7 +75,13 @@ module.exports = {
|
|||
}),
|
||||
new htmlPlugin({
|
||||
template: `${__dirname}/index/index.ejs`,
|
||||
filename: "index.ejs"
|
||||
filename: "index.ejs",
|
||||
chunks: ["index"]
|
||||
}),
|
||||
new htmlPlugin({
|
||||
template: `${__dirname}/table/table.ejs`,
|
||||
filename: "table.ejs",
|
||||
chunks: ["table"]
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
|
|
|
@ -2538,7 +2538,7 @@ jquery-ui@^1.12.1:
|
|||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
|
||||
|
||||
jquery@<3.0.0, jquery@>=1.9.0:
|
||||
jquery@<3.0.0, jquery@>=1.2.6, jquery@>=1.9.0:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02"
|
||||
|
||||
|
@ -4478,6 +4478,12 @@ svgo@^0.7.0, svgo@^0.7.1:
|
|||
sax "~1.2.1"
|
||||
whet.extend "~0.9.9"
|
||||
|
||||
tablesorter@^2.28.5:
|
||||
version "2.28.5"
|
||||
resolved "https://registry.yarnpkg.com/tablesorter/-/tablesorter-2.28.5.tgz#94b6f8326e9ff8b001ae7cf57804476caf8b3437"
|
||||
dependencies:
|
||||
jquery ">=1.2.6"
|
||||
|
||||
tapable@^0.2.5, tapable@~0.2.5:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
const ejs = require("ejs");
|
||||
const fs = require("fs");
|
||||
const Promise = require("promise");
|
||||
|
||||
const commonUtils = require("facilmap-frontend/common/utils");
|
||||
const commonFormat = require("facilmap-frontend/common/format");
|
||||
|
||||
const utils = require("./utils");
|
||||
|
||||
const table = module.exports = {
|
||||
createTable(database, padId, template) {
|
||||
return utils.promiseAuto({
|
||||
padData: database.getPadData(padId),
|
||||
|
||||
types: () => {
|
||||
var types = { };
|
||||
return utils.streamEachPromise(database.getTypes(padId), function(type) {
|
||||
types[type.id] = type;
|
||||
type.markers = [];
|
||||
type.lines = [];
|
||||
}).then(() => types);
|
||||
},
|
||||
|
||||
markers: (types) => {
|
||||
return utils.streamEachPromise(database.getPadMarkers(padId), function(marker) {
|
||||
types[marker.typeId].markers.push(marker);
|
||||
});
|
||||
},
|
||||
|
||||
lines: (types) => {
|
||||
return utils.streamEachPromise(database.getPadLines(padId), function(line) {
|
||||
types[line.typeId].lines.push(line);
|
||||
});
|
||||
}
|
||||
}).then((results) => {
|
||||
return ejs.render(template, {
|
||||
padData: results.padData,
|
||||
types: results.types,
|
||||
utils: commonUtils,
|
||||
format: commonFormat
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
|
@ -8,6 +8,7 @@ const Promise = require("promise");
|
|||
|
||||
const database = require("./database/database");
|
||||
const gpx = require("./gpx");
|
||||
const table = require("./table");
|
||||
const utils = require("./utils");
|
||||
|
||||
const frontendPath = path.dirname(require.resolve("facilmap-frontend/package.json")); // Do not resolve main property
|
||||
|
@ -80,6 +81,7 @@ const webserver = module.exports = {
|
|||
|
||||
app.get("/", padMiddleware);
|
||||
app.get("/index.ejs", padMiddleware);
|
||||
app.get("/table.ejs", padMiddleware);
|
||||
|
||||
app.use(staticMiddleware);
|
||||
|
||||
|
@ -104,6 +106,27 @@ const webserver = module.exports = {
|
|||
}).catch(next);
|
||||
});
|
||||
|
||||
app.get("/:padId/table", function(req, res, next) {
|
||||
Promise.resolve().then(() => {
|
||||
if (process.env.FM_DEV) {
|
||||
let intercept = utils.interceptWriteStream(res);
|
||||
req.url = req.originalUrl = "/table.ejs";
|
||||
staticMiddleware(req, res, next);
|
||||
return intercept;
|
||||
} else {
|
||||
// We don't want express.static's ETag handling, as it sometimes returns an empty template,
|
||||
// so we have to read it directly from the file system
|
||||
|
||||
return Promise.denodeify(fs.readFile)(`${frontendPath}/build/table.ejs`, "utf8");
|
||||
}
|
||||
}).then((template) => {
|
||||
return table.createTable(database, req.params.padId, template);
|
||||
}).then((renderedTable) => {
|
||||
res.type("html");
|
||||
res.send(renderedTable);
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
let server = http.createServer(app);
|
||||
return Promise.denodeify(server.listen.bind(server))(port, host).then(() => server);
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue