This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community. Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it. https://www.freifunk-altdorf.de
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

417 lines
13 KiB

var Class = require('../../core/class');
var Color = require('../../core/color');
var Mode = require('../../modes/current');
var Rectangle = require('../../shapes/rectangle');
// Regular Expressions
var matchURL = /^\s*url\(["'\s]*([^\)]*?)["'\s]*\)/,
requiredNumber = '(?:\\s+|\\s*,\\s*)([^\\s,\\)]+)';
number = '(?:' + requiredNumber + ')?',
matchViewBox = new RegExp('^\\s*([^\\s,]+)' + requiredNumber + requiredNumber + requiredNumber),
matchUnit = /^\s*([\+\-\d\.]+(?:e\d+)?)(|px|em|ex|in|pt|pc|mm|cm|%)\s*$/i;
// Environment Settings
var dpi = 72, emToEx = 0.5;
var styleSheet = function(){},
defaultStyles = {
'viewportWidth': 500,
'viewportHeight': 500,
'font-family': 'Arial',
'font-size': 12,
'color': 'black',
'fill': 'black'
},
nonInheritedStyles = {
'stop-color': 'black',
'stop-opacity': 1,
'clip-path': null,
'filter': null,
'mask': null,
'opacity': 1,
'cursor': null
};
// Visitor
var SVGParser = Class({
initialize: function(mode){
this.MODE = mode;
},
// TODO Fix this silly API
parseAsSurface: function(element, styles){
return this.parse(element, styles, true);
},
parse: function(element, styles, asSurface){
if (typeof element == 'string') element = this.parseXML(element);
if (!styles)
styles = this.findStyles(element);
else
for (var style in defaultStyles)
if (!(style in styles))
styles[style] = defaultStyles[style];
if (element.documentElement || asSurface){
element = element.documentElement || element;
var canvas = new this.MODE.Surface(
this.parseLength(element.getAttribute('width') || '100%', styles, 'x'),
this.parseLength(element.getAttribute('height') || '100%', styles, 'y')
);
if (element.getAttribute('viewBox'))
canvas.grab(this.parse(element, styles));
else
this.container(element, this.parseStyles(element, styles), canvas);
return canvas;
}
if (element.nodeType != 1 || element.getAttribute('requiredExtensions') || element.getAttribute('systemLanguage') != null) return null;
styles = this.parseStyles(element, styles);
var parseFunction = this[element.nodeName + 'Element'];
return parseFunction ? parseFunction.call(this, element, styles) : null;
},
parseXML: window.DOMParser ? function(text){
return new DOMParser().parseFromString(text, 'text/xml');
} : function(text){
try {
var xml;
try { xml = new ActiveXObject('MSXML2.DOMDocument'); }
catch (e){ xml = new ActiveXObject('Microsoft.XMLDOM'); }
xml.resolveExternals = false;
xml.validateOnParse = false;
xml.async = false;
xml.preserveWhiteSpace = true;
xml.loadXML(text);
return xml;
} catch (e){
return null;
}
},
parseStyles: function(element, styles){
styleSheet.prototype = styles;
var newSheet = new styleSheet();
for (var key in nonInheritedStyles) newSheet[key] = nonInheritedStyles[key];
this.applyStyles(element, newSheet);
if (newSheet.hasOwnProperty('font-size')){
var newFontSize = this.parseLength(newSheet['font-size'], styles, 'font');
if (newFontSize != null) newSheet['font-size'] = newFontSize;
}
if (newSheet.hasOwnProperty('text-decoration')){
newSheet['text-decoration-color'] = newSheet.color;
}
return newSheet;
},
findStyles: function(element){
if (!element || element.nodeType != 1) return defaultStyles;
var styles = this.findStyles(element.parentNode);
return this.parseStyles(element, styles);
},
applyStyles: function(element, target){
var attributes = element.attributes;
for (var i = 0, l = attributes.length; i < l; i++){
var attribute = attributes[i],
name = attribute.nodeName,
value = attribute.nodeValue;
if (value != 'inherit'){
target[name] = value;
if (name == 'fill') target['fill_document'] = element.ownerDocument;
}
}
return target;
},
findById: function(document, id){
// if (document.getElementById) return document.getElementById(id); Not reliable
if (this.cacheDocument != document){
this.ids = {};
this.lastSweep = null;
this.cacheDocument = document;
}
var ids = this.ids;
if (ids[id] != null) return ids[id];
var root = document.documentElement, node = this.lastSweep || root;
treewalker: while (node){
if (node.nodeType == 1){
var newID = node.getAttribute('id') || node.getAttribute('xml:id');
if (newID && ids[newID] == null) ids[newID] = node;
if (newID == id){
this.lastSweep = node;
return node;
}
}
if (node.firstChild){
node = node.firstChild;
} else {
while (!node.nextSibling){
node = node.parentNode;
if (!node || node == root) break treewalker;
}
node = node.nextSibling;
}
}
return null;
},
findByURL: function(document, url, callback){
callback.call(this, url && url[0] == '#' ? this.findById(document, url.substr(1)) : null);
},
resolveURL: function(url){
return url;
},
parseLength: function(value, styles, dimension){
var match = matchUnit.exec(value);
if (!match) return null;
var result = parseFloat(match[1]);
switch(match[2]){
case '': case 'px': return result;
case 'em': return result * styles['font-size'];
case 'ex': return result * styles['font-size'] * emToEx;
case 'in': return result * dpi;
case 'pt': return result * dpi / 72;
case 'pc': return result * dpi / 6;
case 'mm': return result * dpi / 25.4;
case 'cm': return result * dpi / 2.54;
case '%':
var w = styles.viewportWidth, h = styles.viewportHeight;
if (dimension == 'font') return result * styles['font-size'] / 100;
if (dimension == 'x') return result * w / 100;
if (dimension == 'y') return result * h / 100;
return result * Math.sqrt(w * w + h * h) / Math.sqrt(2) / 100;
}
},
parseColor: function(value, opacity, styles){
if (value == 'currentColor') value = styles.color;
try {
var color = new Color(value);
} catch (x){
// Ignore unparsable colors, TODO: log
return null;
}
color.alpha = opacity == null ? 1 : +opacity;
return color;
},
getLengthAttribute: function(element, styles, attr, dimension){
return this.parseLength(element.getAttribute(attr) || 0, styles, dimension);
},
container: function(element, styles, container){
if (container.width != null) styles.viewportWidth = container.width;
if (container.height != null) styles.viewportHeight = container.height;
this.filter(styles, container);
this.describe(element, styles, container);
var node = element.firstChild;
while (node){
var art = this.parse(node, styles);
if (art) container.grab(art);
node = node.nextSibling;
}
return container;
},
shape: function(element, styles, target, x, y){
this.transform(element, target);
target.transform(1, 0, 0, 1, x, y);
this.fill(styles, target, x, y);
this.stroke(styles, target);
this.filter(styles, target);
if (styles.visibility == 'hidden') target.hide();
this.describe(element, styles, target);
return target;
},
fill: function(styles, target, x, y){
if (!styles.fill || styles.fill == 'none') return;
var match;
if (match = matchURL.exec(styles.fill)){
this.findByURL(styles.fill_document, match[1], function(fill){
var fillFunction = fill && this[fill.nodeName + 'Fill'];
if (fillFunction) fillFunction.call(this, fill, this.findStyles(fill), target, x, y);
});
} else {
target.fill(this.parseColor(styles.fill, styles['fill-opacity'], styles));
}
},
stroke: function(styles, target){
if (!styles.stroke || styles.stroke == 'none' || matchURL.test(styles.stroke)) return; // Advanced stroke colors are not supported, TODO: log
var color = this.parseColor(styles.stroke, styles['stroke-opacity'], styles),
width = this.parseLength(styles['stroke-width'], styles),
cap = styles['stroke-linecap'] || 'butt',
join = styles['stroke-linejoin'] || 'miter';
target.stroke(color, width == null ? 1 : width, cap, join);
},
filter: function(styles, target){
if (styles.opacity != 1 && target.blend) target.blend(styles.opacity);
if (styles.display == 'none') target.hide();
},
describe: function(element, styles, target){
var node = element.firstChild, title = '';
if (element.nodeName != 'svg')
while (node){
if (node.nodeName == 'title') title += node.firstChild && node.firstChild.nodeValue;
node = node.nextSibling;
}
if (styles.cursor || title) target.indicate(styles.cursor, title);
},
transform: function(element, target){
var transform = element.getAttribute('transform'), match;
var matchTransform = new RegExp('([a-z]+)\\s*\\(\\s*([^\\s,\\)]+)' + number + number + number + number + number + '\\s*\\)', 'gi');
while(match = transform && matchTransform.exec(transform)){
switch(match[1]){
case 'matrix':
target.transform(match[2], match[3], match[4], match[5], match[6], match[7]);
break;
case 'translate':
target.transform(1, 0, 0, 1, match[2], match[3]);
break;
case 'scale':
target.transform(match[2], 0, 0, match[3] || match[2]);
break;
case 'rotate':
var rad = match[2] * Math.PI / 180, cos = Math.cos(rad), sin = Math.sin(rad);
target.transform(1, 0, 0, 1, match[3], match[4])
.transform(cos, sin, -sin, cos)
.transform(1, 0, 0, 1, -match[3], -match[4]);
break;
case 'skewX':
target.transform(1, 0, Math.tan(match[2] * Math.PI / 180), 1);
break;
case 'skewY':
target.transform(1, Math.tan(match[2] * Math.PI / 180), 0, 1);
break;
}
}
},
svgElement: function(element, styles){
var viewbox = element.getAttribute('viewBox'),
match = matchViewBox.exec(viewbox),
x = this.getLengthAttribute(element, styles, 'x', 'x'),
y = this.getLengthAttribute(element, styles, 'y', 'y'),
width = this.getLengthAttribute(element, styles, 'width', 'x'),
height = this.getLengthAttribute(element, styles, 'height', 'y'),
group = match ? new this.MODE.Group(+match[3], +match[4]) : new this.MODE.Group(width || null, height || null);
if (width && height) group.resizeTo(width, height); // TODO: Aspect ratio
if (match) group.transform(1, 0, 0, 1, -match[1], -match[2]);
this.container(element, styles, group);
group.move(x, y);
return group;
},
gElement: function(element, styles){
var group = new this.MODE.Group();
this.transform(element, group);
this.container(element, styles, group);
return group;
},
useElement: function(element, styles){
var placeholder = new this.MODE.Group(),
x = this.getLengthAttribute(element, styles, 'x', 'x'),
y = this.getLengthAttribute(element, styles, 'y', 'y'),
width = this.getLengthAttribute(element, styles, 'width', 'x'),
height = this.getLengthAttribute(element, styles, 'height', 'y');
this.transform(element, placeholder);
placeholder.transform(1, 0, 0, 1, x, y);
this.findByURL(element.ownerDocument, element.getAttribute('xlink:href') || element.getAttribute('href'), function(target){
if (!target || target.nodeType != 1) return;
var parseFunction = target.nodeName == 'symbol' ? this.svgElement : this[target.nodeName + 'Element'];
if (!parseFunction) return;
styles = this.parseStyles(element, this.parseStyles(target, styles));
var symbol = parseFunction.call(this, target, styles);
if (!symbol) return;
if (width && height) symbol.resizeTo(width, height); // TODO: Aspect ratio, maybe resize the placeholder instead
placeholder.grab(symbol);
});
return placeholder;
},
switchElement: function(element, styles){
var node = element.firstChild;
while (node){
var art = this.parse(node, styles);
if (art) return art;
node = node.nextSibling;
}
return null;
},
aElement: function(element, styles){
// For now treat it like a group
return this.gElement(element, styles);
},
pathElement: function(element, styles){
var shape = new this.MODE.Shape(element.getAttribute('d') || null);
this.shape(element, styles, shape);
return shape;
},
imageElement: function(element, styles){
var href = this.resolveURL(element.getAttribute('xlink:href') || element.getAttribute('href')),
width = this.getLengthAttribute(element, styles, 'width', 'x'),
height = this.getLengthAttribute(element, styles, 'height', 'y'),
x = this.getLengthAttribute(element, styles, 'x', 'x'),
y = this.getLengthAttribute(element, styles, 'y', 'y'),
clipPath = element.getAttribute('clip-path'),
image,
match;
if (clipPath && (match = matchURL.exec(clipPath)) && match[1][0] == '#'){
var clip = this.findById(element.ownerDocument, match[1].substr(1));
if (clip){
image = this.switchElement(clip, styles);
if (image){
if (typeof image.fillImage == 'function'){
image.fillImage(href, width, height);
if (image.stroke) image.stroke(0);
} else {
image = null;
}
}
}
}
if (!image){
//image = new Image(href, width, height); TODO
image = new Rectangle(width, height).fillImage(href, width, height);
}
this.filter(styles, image);
if (styles.visibility == 'hidden') target.hide();
this.describe(element, styles, image);
this.transform(element, image);
image.transform(1, 0, 0, 1, x, y);
return image;
}
});
SVGParser.parse = function(element, styles){
return new SVGParser(Mode).parse(element, styles);
};
SVGParser.implement = function(obj){
for (var key in obj)
this.prototype[key] = obj[key];
};
module.exports = SVGParser;