JavaScript实现将SLD和JSON互转(JavaScript implementation to convert SLD and JSON into each other)

41 篇文章 0 订阅

在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>

涉及到的GraduatedSymbolSldCategorizedSymbolSldSingleSymbolSldBaseSymbolSld的继承实现,请参后续文章。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丷丩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值