在GeoServer中发布样式用到SLD,需要在业务中进行SLD的生成。
SLD本身是特定的XML。
JavaScript处理JSON数据比较灵活,但是对于XML处理相对复杂一点。
因此将SLD转换为JSON进行处理。
由于SLD涉及到的标签非常多,规则比较灵活,目前只支持单一符号、分类符号和等级符号的SLD以及文本标注的SLD。
效果测试页面:
BaseSymbolSld类:
import { CompareisonModeNameConvert } from "./filterFactory.js";
import SymbolFactory from "./symbolFactory.js";
/**
* 通过JSON创建SLD文件的基类
* @author rzc
* @updated 2024年4月24日
*/
class BaseSymbolSld {
_options = {};
_encoding = "utf8";
/**
* @description 编码格式
*/
get encoding() {
return this._encoding;
}
set encoding(val) {
this._encoding = val;
}
_xmlInfo = `<?xml version="1.0" encoding="${this._encoding}"?>`;
/**
* 字符串格式的StyledLayerDescriptor
* @param {String} content StyledLayerDescriptor内的元素
* @returns 字符串格式的StyledLayerDescriptor
*/
createStyledLayerDescriptor(content) {
return `<StyledLayerDescriptor xmlns='http://www.opengis.net/sld'
xmlns:ogc='http://www.opengis.net/ogc' xmlns:xlink='http://www.w3.org/1999/xlink'
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' version='1.0.0'
xsi:schemaLocation='http://www.opengis.net/sld StyledLayerDescriptor.xsd'
xmlns:sld='http://www.opengis.net/sld'>${content}</StyledLayerDescriptor>`;
}
/**
* 字符串格式的 createNamedLayer
* @param {String} name Name 内的文本
* @param {String} userStyle createNamedLayer 内的元素
* @returns 字符串格式的 createNamedLayer
*/
createNamedLayer(name, userStyle) {
return `<NamedLayer><Name>${name}</Name><UserStyle>${userStyle}</UserStyle></NamedLayer>`;
}
/**
* FeatureTypeStyle 元素的字符串表示
* @param {String} rules 全部rule的字符串表示
* @returns 字符串
*/
createFeatureTypeStyle(rules) {
return `<FeatureTypeStyle>${rules}</FeatureTypeStyle>`;
}
/**
* 构造函数
* @param {Object} options Json格式的样式表达
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(options) {
this._options = options;
}
/**
* 生成一个规则样式
* @param {Object} rule Rule
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
createRule(rule) {
throw new Error("not implemented");
}
/**
* 创建Rule的XML元素
* @param {Object} rule 单一符号规则
*/
createRuleSingle(rule, index) {
return `<Rule>
<Name>${rule.Name || ("Rule " + index)}</Name>
${rule.isElseFilter ? "<ElseFilter/>" : ""}
${this.createSymbol(rule.symbol)}
</Rule>`;
}
/**
* 生成注记规则
* @param {Object} rule 规则
* @returns 生成注记规则
*/
createLabelRule(rule) {
if (!rule.isLabel) return "";
let type = "Point";
if (this._options.geoType === "Line") type = "Line";
return `<Rule>
<Name>标注</Name>
${SymbolFactory.createLabelSymbol(rule.symbol, type)}
</Rule>`;
}
/**
* 生成全部规则的XML字符串表达
* @returns 全部规则的XML字符串表达
*/
createRules() {
let rulesXMLStr = "";
if (!this._options?.rules) {
throw Error(`×××××××× Invalid rules: 必须指定规则Rules`);
}
if (!this._options?.rules.length || this._options?.rules.length === 0) {
throw Error(`×××××××× Invalid rules: 长度大于0`);
}
for (let i = 0; i < this._options?.rules.length; i++) {
const rule = this._options.rules[i];
if (rule.isElseFilter) rulesXMLStr += this.createRuleSingle(rule, i);
else if (rule.isLabel) rulesXMLStr += this.createLabelRule(rule);
else rulesXMLStr += this.createRule(rule, i);
}
return rulesXMLStr;
}
/**
* 生成XML字符串表达
* @returns XML字符串表达
*/
create() {
const rulesXML = this.createRules();
const featureTypeStyle = this.createFeatureTypeStyle(rulesXML);
const namedLayer = this.createNamedLayer(this._options.name, featureTypeStyle);
const styledLayerDescriptor = this.createStyledLayerDescriptor(namedLayer);
const result = FormatXML(`${this._xmlInfo}${styledLayerDescriptor}`);
return result;
}
/**
* 创建符号的XML字符串
* @param {Object} symbol 符号的json表达
* @returns 符号的XML字符串
*/
createSymbol(symbol) {
if (this._options.geoType === "Point") return SymbolFactory.createPointSymbol(symbol);
else if (this._options.geoType === "Line") return SymbolFactory.createLineSymbol(symbol);
else if (this._options.geoType === "Polygon") return SymbolFactory.createPolygonSymbol(symbol);
else throw Error(`×××××××× Invalid geoType: geoType必须指定为Point/Line/Polygon之一`);
}
}
/**
* 格式化XML字符串
* @param {String} xmlStr 字符串格式的XML
* @returns 格式化后的XML字符串
*/
const FormatXML = (xmlStr) => {
// 计算头函数 用来缩进
const setPrefix = (prefixIndex) => {
let result = "";
let span = " "; // 缩进长度
var output = [];
for (var i = 0; i < prefixIndex; ++i) {
output.push(span);
}
result = output.join("");
return result;
};
let text = xmlStr;
// 使用replace去空格
const replaceParaCb = ($0, name, props) => {
return name + " " + props.replace(/\s+(\w+=)/g, " $1");
};
text = "\n" + text.replace(/(<\w+)(\s.*?>)/g, replaceParaCb).replace(/>\s*?</g, ">\n<");
// 处理注释
const replaceParaCb2 = ($0, text) => {
return "<!--" + escape(text) + "-->";
};
text = text
.replace(/\n/g, "\r")
.replace(/<!--(.+?)-->/g, replaceParaCb2)
.replace(/\r/g, "\n");
// 调整格式 以压栈方式递归调整缩进
var rgx = /\n(<(([^\\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/gm;
var nodeStack = [];
var output = text.replace(rgx, function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
var isClosed = isCloseFull1 === "/" || isCloseFull2 === "/" || isFull1 === "/" || isFull2 === "/";
var prefix = "";
if (isBegin === "!") {
prefix = setPrefix(nodeStack.length); //! 开头
} else {
if (isBegin !== "/") {
prefix = setPrefix(nodeStack.length); // /开头
if (!isClosed) {
nodeStack.push(name);
} // 非关闭标签
} else {
nodeStack.pop(); // 弹栈
prefix = setPrefix(nodeStack.length);
}
}
var ret = "\n" + prefix + all;
return ret;
});
var outputText = output.substring(1);
// 还原注释内容
outputText = outputText.replace(/\n/g, "\r").replace(/(\s*)<!--(.+?)-->/g, function ($0, prefix, text) {
if (prefix.charAt(0) === "\r") prefix = prefix.substring(1);
text = unescape(text).replace(/\r/g, "\n");
return "\n" + prefix + "<!--" + text.replace(/^\s*/gm, prefix) + "-->";
});
outputText = outputText.replace(/\s+$/g, "").replace(/\r/g, "\r\n");
return outputText;
};
/**
* xml格式化的过滤条件转换为json格式.过滤条件
* @param {XMLDocument} filterElement 过滤条件
* @returns json格式过滤条件
*/
const filterToJson = (filterElement) => {
const filter = {};
const andEle = filterElement.getElementsByTagName("ogc:And")[0];
const findNodeEle = (pNode, startIndex) => {
for (let index = startIndex; index < pNode.childNodes.length; index++) {
const ele = pNode.childNodes[index];
if (ele.nodeName.indexOf("Property") > -1) {
return index;
}
}
return -1;
};
if (andEle) {
const firstEleIdx = findNodeEle(andEle, 0);
const matchEle1 = andEle.childNodes[firstEleIdx];
if (!matchEle1) {
throw Error(`×××××××× Invalid ogc:And: 至少应当包含两个子元素`);
}
const secondEleIdx = findNodeEle(andEle, firstEleIdx + 1);
const matchEle2 = andEle.childNodes[secondEleIdx];
if (!matchEle2) {
throw Error(`×××××××× Invalid ogc:And: 至少应当包含两个子元素`);
}
filter.propertyName = filterElement.getElementsByTagName("ogc:PropertyName")[0].textContent;
filter.matchType1 = CompareisonModeNameConvert(matchEle1.nodeName);
filter.propertyValue1 = filterElement.getElementsByTagName("ogc:Literal")[0].textContent;
filter.matchType2 = CompareisonModeNameConvert(matchEle2.nodeName);
filter.propertyValue2 = filterElement.getElementsByTagName("ogc:Literal")[1].textContent;
} else {
const firstEleIdx = findNodeEle(filterElement, 0);
const ele = filterElement.childNodes[firstEleIdx];
filter.matchType = CompareisonModeNameConvert(ele.nodeName);
filter.propertyName = ele.getElementsByTagName("ogc:PropertyName")[0].textContent;
filter.propertyValue = ele.getElementsByTagName("ogc:Literal")[0].textContent;
}
return filter;
};
/**
* xml格式化的点符号转换为json格式.点符号
* @param {XMLDocument} pointSymbolizerEle 点符号
* @returns json格式点符号
*/
const pointSymbolToJson = (pointSymbolizerEle) => {
if (!pointSymbolizerEle) return null;
const point = {};
const markEle = pointSymbolizerEle.getElementsByTagName("Mark")[0];
if (markEle) {
point.wellKnownName = markEle.getElementsByTagName("WellKnownName")[0].textContent;
point.fill = markEle.querySelector("[name='fill']").textContent;
point.fillOpacity = markEle.querySelector("[name='fill-opacity']").textContent;
point.stroke = markEle.querySelector("[name='stroke']").textContent;
point.strokeWidth = markEle.querySelector("[name='stroke-width']").textContent;
point.strokeOpacity = markEle.querySelector("[name='stroke-opacity']").textContent;
} else {
point.base64Content = pointSymbolizerEle.getElementsByTagName("InlineContent")[0].textContent;
point.anchorPointX = pointSymbolizerEle.getElementsByTagName("AnchorPointX")[0].textContent;
point.anchorPointY = pointSymbolizerEle.getElementsByTagName("AnchorPointY")[0].textContent;
}
point.opacity = pointSymbolizerEle.getElementsByTagName("Opacity")[0].textContent;
point.size = pointSymbolizerEle.getElementsByTagName("Size")[0].textContent;
point.rotation = pointSymbolizerEle.getElementsByTagName("Rotation")[0].textContent;
return point;
};
/**
* xml格式化的线符号转换为json格式线符号
* @param {XMLDocument} lineSymbolizerEle 线符号
* @returns json格式线符号
*/
const lineSymbolToJson = (lineSymbolizerEle) => {
if (!lineSymbolizerEle) return null;
const line = {};
line.stroke = lineSymbolizerEle.querySelector("[name='stroke']")?.textContent;
line.strokeWidth = lineSymbolizerEle.querySelector("[name='stroke-width']")?.textContent;
line.strokeOpacity = lineSymbolizerEle.querySelector("[name='stroke-opacity']")?.textContent;
line.strokeLineJoin = lineSymbolizerEle.querySelector("[name='stroke-linejoin']")?.textContent;
line.strokeLineCap = lineSymbolizerEle.querySelector("[name='stroke-linecap']")?.textContent;
line.strokeDashArray = lineSymbolizerEle.querySelector("[name='stroke-dasharray']")?.textContent;
return line;
};
/**
* xml格式化的面符号转换为json格式面符号
* @param {XMLDocument} polygonSymbolizerEle 面符号
* @returns json格式面符号
*/
const polygonSymbolToJson = (polygonSymbolizerEle) => {
if (!polygonSymbolizerEle) return null;
const poly = {};
poly.fill = polygonSymbolizerEle.querySelector("[name='fill']")?.textContent;
poly.fillOpacity = polygonSymbolizerEle.querySelector("[name='fill-opacity']")?.textContent;
poly.stroke = polygonSymbolizerEle.querySelector("[name='stroke']")?.textContent;
poly.strokeWidth = polygonSymbolizerEle.querySelector("[name='stroke-width']")?.textContent;
poly.strokeOpacity = polygonSymbolizerEle.querySelector("[name='stroke-opacity']")?.textContent;
return poly;
};
/**
* xml格式化的文本符号转换为json格式文本符号
* @param {XMLDocument} textSymbolizerEle 文本符号
* @returns json格式文本符号
*/
const textSymbolToJson = (textSymbolizerEle) => {
if (!textSymbolizerEle) return null;
const text = {};
const linePlacement = textSymbolizerEle.getElementsByTagName("LinePlacement")[0];
if (linePlacement) {
text.perpendicularOffset = textSymbolizerEle.getElementsByTagName("PerpendicularOffset")[0]?.textContent;
} else {
text.anchorPointX = textSymbolizerEle.getElementsByTagName("AnchorPointX")[0]?.textContent;
text.anchorPointY = textSymbolizerEle.getElementsByTagName("AnchorPointY")[0]?.textContent;
text.displacementX = textSymbolizerEle.getElementsByTagName("DisplacementX")[0]?.textContent;
text.displacementY = textSymbolizerEle.getElementsByTagName("DisplacementY")[0]?.textContent;
text.rotation = textSymbolizerEle.getElementsByTagName("Rotation")[0]?.textContent;
}
text.propertyName = textSymbolizerEle.getElementsByTagName("ogc:PropertyName")[0]?.textContent;
text.fontFamily = textSymbolizerEle.querySelector("[name='font-family']")?.textContent;
text.fontSize = textSymbolizerEle.querySelector("[name='font-size']")?.textContent;
text.fill = textSymbolizerEle.querySelector("[name='fill']").textContent;
text.autoWrap = textSymbolizerEle.querySelector("[name='autoWrap']")?.textContent;
text.group = textSymbolizerEle.querySelector("[name='group']")?.textContent;
return text;
};
/**
* xml格式化的规则转换为json格式规则
* @param {XMLDocument} ruleElement 规则
* @returns json格式规则
*/
const ruleToJson = (ruleElement) => {
const rule = {};
const nameElement = ruleElement.getElementsByTagName("Name")[0];
if (nameElement) {
rule.name = nameElement.textContent?.trim();
}
const filterElement = ruleElement.getElementsByTagName("ogc:Filter")[0];
if (filterElement) {
rule.filter = filterToJson(filterElement);
}
const elseFilterEle = ruleElement.getElementsByTagName("ElseFilter")[0];
const textSymbolizerEle = ruleElement.getElementsByTagName("TextSymbolizer")[0];
const textSymbolizer = textSymbolToJson(textSymbolizerEle);
!rule.symbol && (rule.symbol = textSymbolizer) && (rule.isLabel = true);
const pointSymbolizerEle = ruleElement.getElementsByTagName("PointSymbolizer")[0];
!rule.symbol && (rule.symbol = pointSymbolToJson(pointSymbolizerEle));
const lineSymbolizerEle = ruleElement.getElementsByTagName("LineSymbolizer")[0];
!rule.symbol && (rule.symbol = lineSymbolToJson(lineSymbolizerEle));
const polygonSymbolizerEle = ruleElement.getElementsByTagName("PolygonSymbolizer")[0];
!rule.symbol && (rule.symbol = polygonSymbolToJson(polygonSymbolizerEle));
rule.symbol && elseFilterEle && (rule.isElseFilter = true);
lineSymbolizerEle && (rule.geoType = "Line");
pointSymbolizerEle && (rule.geoType = "Point");
polygonSymbolizerEle && (rule.geoType = "Polygon");
for (var item in rule.symbol) {
const itemValue = rule.symbol[item];
if (typeof itemValue === "string") rule.symbol[item] = rule.symbol[item].trim();
}
return rule;
};
/**
* xml格式化的SLD转换为json格式
* @param {XMLDocument} xmlSldStr SLD规则
* @returns json格式
*/
const SldToRuleJson = (xmlSldStr) => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlSldStr, "application/xml");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const featureTypeStyles = xmlDoc.getElementsByTagName("FeatureTypeStyle")[0];
if (!featureTypeStyles) {
throw Error(`×××××××× Invalid SLD String : SLD 必须包含FeatureTypeStyle节点`);
}
const nameElement = xmlDoc.getElementsByTagName("Name")[0];
if (!nameElement) {
throw Error(`×××××××× Invalid SLD String : SLD 必须包含Name节点`);
}
const result = { name: nameElement.textContent, rules: [] };
const rulesElements = featureTypeStyles.getElementsByTagName("Rule");
if (rulesElements.length === 0) {
throw Error(`×××××××× Invalid SLD String : SLD 必须包含Rule节点`);
}
for (let i = 0; i < rulesElements.length; i++) {
const ruleEle = rulesElements[i];
const ruleJson = ruleToJson(ruleEle);
ruleJson.geoType && (result.geoType = ruleJson.geoType);
result.rules.push(ruleJson);
}
return result;
};
export { BaseSymbolSld, FormatXML, SldToRuleJson };
测试页面test.html:
<!DOCTYPE html>
<head>
<style type="text/css">
html,
body {
height: 100% !important;
margin: 0;
padding: 0%;
overflow: hidden;
}
.middle {
height: 100%;
width: 200px;
display: flex;
flex-wrap: wrap;
align-content: center;
justify-content: center;
}
.middle button {
height: 40px;
display: block;
}
.left,
.right {
height: 100%;
width: calc(50% - 100px - 2px);
text-align: center;
border: 1px solid #eee;
}
textarea {
width: 100%;
height: 99%
}
</style>
</head>
<body>
<div style="border-bottom: 1px solid #ccc;">
测试从XML格式的SLD转换为JSON、从JSON转换为XML
</div>
<div style="display: flex;width: 100%;height: 95%;flex-wrap: wrap;flex: 1 auto 1">
<div class="left">
<span>XML:</span>
<textarea type="text" id="xmlInput"></textarea>
</div>
<div class="middle">
<button id="btnSLD2JSON">SLD =>> JSON</button>
<button id="btnJSON2SLD_G">JSON <<= SLD(等级符号)</button>
<button id="btnJSON2SLD_C">JSON <<= SLD(分类符号)</button>
<button id="btnJSON2SLD_S">JSON <<= SLD(单一符号)</button>
</div>
<div class="right">
<span>JSON:</span>
<textarea type="text" id="jsonInput"></textarea>
</div>
</div>
<script type="module">
import { SldToRuleJson } from "./baseSymbolSld.js";
import { GraduatedSymbolSld } from "./graduatedSymbolSld.js"
import { CategorizedSymbolSld } from "./categorizedSymbolSld.js";
import { SingleSymbolSld } from "./singleSymbolSld.js";
const btnSLD2JSON = document.getElementById("btnSLD2JSON");
const btnJSON2SLD_G = document.getElementById("btnJSON2SLD_G");
const btnJSON2SLD_C = document.getElementById("btnJSON2SLD_C");
const btnJSON2SLD_S = document.getElementById("btnJSON2SLD_S");
const xmlInputElm = document.getElementById("xmlInput");
const jsonInputElm = document.getElementById("jsonInput");
btnSLD2JSON.onclick = () => {
const jsonObj = SldToRuleJson(xmlInputElm.value);
jsonInputElm.value = JSON.stringify(jsonObj, null, " ");
}
const getJSONInput = () => {
const jsonStr = jsonInputElm.value;
const jsonObj = JSON.parse(jsonStr);
return jsonObj;
}
btnJSON2SLD_G.onclick = () => {
///
const graduatedSymbolSld = new GraduatedSymbolSld(getJSONInput());
const xmlStr = graduatedSymbolSld.create();
///
xmlInputElm.value = xmlStr;
}
btnJSON2SLD_C.onclick = () => {
///
const categorizedSymbolSld = new CategorizedSymbolSld(getJSONInput());
const xmlStr = categorizedSymbolSld.create();
///
xmlInputElm.value = xmlStr;
}
btnJSON2SLD_S.onclick = () => {
///
const singleSymbolSld = new SingleSymbolSld(getJSONInput());
const xmlStr = singleSymbolSld.create();
///
xmlInputElm.value = xmlStr;
}
</script>
</body>
涉及到的GraduatedSymbolSld
、CategorizedSymbolSld
、SingleSymbolSld
是BaseSymbolSld
的继承实现,请参后续文章。