Skip to content

Instantly share code, notes, and snippets.

@wewindy
Created May 24, 2022 10:17
Show Gist options
  • Select an option

  • Save wewindy/2185a33f0d9691b264e4a897093c6fff to your computer and use it in GitHub Desktop.

Select an option

Save wewindy/2185a33f0d9691b264e4a897093c6fff to your computer and use it in GitHub Desktop.
WKTString-Geojson Transformer
export interface IGeoJson {
type: 'Feature' | 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection';
coordinates?: any[];
geometry?: IGeoJson;
geometries?: IGeoJson[];
crs?: string | {
type: string;
properties: {
name: string;
};
};
}
export interface IWKTCoord extends Array<number> {
}
export interface IWKTRing extends Array<IWKTCoord> {
}
export interface IWKTRings extends Array<IWKTRing> {
}
export interface IWKTMultiRings extends Array<any> {
}
/**
* Stringifies a GeoJSON object into WKT
*/
export declare const stringify: (geojson: IGeoJson) => any;
/**
* Parse WKT and return GeoJSON.
*
* @param input A EWKT/WKT geometry
*/
export declare const parse: (wktString: string) => IGeoJson;
// 数字格式,含可选的正号或负号,支持小数点、e或E表示的科学计数法
const numberRegexp = /[-+]?([0-9]*\.[0-9]+|[0-9]+)([eE][-+]?[0-9]+)?/;
// 至少 1 个以上的数字格式,且由空格连接,例如 “1 -2.98 5e-2” atLeastOneNumberConnectWithSpace
const atLeastOneNumberConnectWithSpace = new RegExp(`^${numberRegexp.source}(\\s${numberRegexp.source}){1,}`);
/**
* @param coords
* @example
* ```
* [112.5, 22.3] => "112.5 22.3"
* ```
*/
const pairWKT = (coords) => coords.join(' ');
/**
* @param ring
* @example
* ```
* [[112.52 22.30], [114.31 21.92]] => "112.52 22.30,114.31 21.92"
* ```
*/
const ringWKT = (ring) => ring.map(pairWKT).join(',');
/**
* @param rings Polygon 或 MultiLineString
* @example
* ```
* [
* [[310, 30], [40, 30], [50, 20]], // ring0
* [[10, 10], [20, 20]] // ring1
* ]
* =>
* "(310 30, 40 30, 50 20),(10 10, 20 20)"
* ```
*/
const ringsWKT = (rings) => rings.map(ringWKT).map(addOutsideBrackets).join(',');
/**
* @param multiRings MULTIPOLYGON
* @example
* ```
* [
* [ // singlePolygon0
* [[0, 1], [3, 0], [4, 3], [0, 4], [0, 1]],
* ],
* [ // singlePolygon1
* [[3, 4], [6, 3], [5, 5], [3, 4]],
* ],
* ]
* =>
* "((0 1,3 0,4 3,0 4,0 1)),((3 4,6 3,5 5,3 4))"
* ```
*/
const multiRingsWKT = (multiRings) => multiRings.map(ringsWKT).map(addOutsideBrackets).join(',');
const addOutsideBrackets = (s) => `(${s})`;
/**
* Stringifies a GeoJSON object into WKT
*/
export const stringify = (geojson) => {
const target = geojson.type === 'Feature' ? geojson.geometry : geojson;
switch (target.type) {
case 'Point':
return `POINT (${pairWKT(target.coordinates)})`;
case 'LineString':
return `LINESTRING (${ringWKT(target.coordinates)})`;
case 'Polygon':
return `POLYGON (${ringsWKT(target.coordinates)})`;
case 'MultiPoint':
return `MULTIPOINT (${ringWKT(target.coordinates)})`;
case 'MultiPolygon':
return `MULTIPOLYGON (${multiRingsWKT(target.coordinates)})`;
case 'MultiLineString':
return `'MULTILINESTRING (${ringsWKT(target.coordinates)})`;
case 'GeometryCollection':
return `GEOMETRYCOLLECTION (${target.geometries.map(stringify).join(',')})`;
default:
throw new Error('stringify requires a valid GeoJSON Feature or geometry object as input');
}
};
/**
* Parse WKT and return GeoJSON.
*
* @param input A EWKT/WKT geometry
*/
export const parse = (wktString) => {
// DEMO: SRID=4269;LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)
const parts = wktString.split(';');
let wktGeometryPart = parts.pop(); // 取表示几何的最后一部分
const srid = (parts.shift() || '').split('=').pop(); // 取表示坐标系的第一部分
let charOffset = 0;
/**
* 使用正则表达式选择 WKT 文本
* @param reg 正则选择器
* @returns 匹配到的字符串
* @private
*/
const $ = (reg) => {
const match = wktGeometryPart.substring(charOffset).match(reg);
if (!match)
return null;
else {
charOffset += match[0].length;
return match[0];
}
};
/**
* 为最终的 GeoJson 附加参考坐标系信息
* @private
*/
const appendCrs = (obj) => {
if (obj && srid.match(/\d+/)) {
obj.crs = {
type: 'name',
properties: {
name: `urn:ogc:def:crs:EPSG::${srid}`
}
};
}
return obj;
};
/**
* 移除开头的空格(*号表示0~N个)
* @private
*/
const white = () => { $(/^\s*/); };
const multicoords = () => {
white();
let depth = 0;
const rings = [];
const stack = [rings];
let pointer = rings;
let elem;
while (elem =
$(/^(\()/) ||
$(/^(\))/) ||
$(/^(,)/) ||
$(atLeastOneNumberConnectWithSpace)) {
if (elem === '(') {
stack.push(pointer);
pointer = [];
stack[stack.length - 1].push(pointer);
depth++;
}
else if (elem === ')') {
// For the case: Polygon(), ...
if (pointer.length === 0)
return null;
pointer = stack.pop();
// the stack was empty, input was malformed
if (!pointer)
return null;
depth--;
if (depth === 0)
break;
}
else if (elem === ',') {
pointer = [];
stack[stack.length - 1].push(pointer);
}
else if (!elem.split(/\s/g).some(v => isNaN(+v))) {
Array.prototype.push.apply(pointer, elem.split(/\s/g).map(parseFloat));
}
else {
return null;
}
white();
}
if (depth !== 0)
return null;
return rings;
};
const coords = () => {
let list = [];
let item;
let pt;
while (pt = $(atLeastOneNumberConnectWithSpace) || $(/^(,)/)) {
if (pt === ',') {
list.push(item);
item = [];
}
else if (!pt.split(/\s/g).some(v => isNaN(+v))) {
if (!item)
item = [];
Array.prototype.push.apply(item, pt.split(/\s/g).map(parseFloat));
}
white();
}
if (item)
list.push(item);
else
return null;
return list.length ? list : null;
};
const point = () => {
// 选择 WKT 中的 'point' 或 'point z'(不区分大小写)
if (!$(/^(point(\sz)?)/i))
return null;
white();
// 选择 WKT 中的 '('
if (!$(/^(\()/))
return null;
// 选择坐标
const c = coords();
if (!c)
return null;
white();
if (!$(/^(\))/))
return null;
return {
type: 'Point',
coordinates: c[0]
};
};
const multipoint = () => {
// 选择 WKT 中的 'multipoint'(不区分大小写)
if (!$(/^(multipoint)/i))
return null;
white();
// 重组成新的 MULTIPOINT WKT
const newCoordsFormat = wktGeometryPart
.substring(wktGeometryPart.indexOf('(') + 1, wktGeometryPart.length - 1)
.replace(/\(/g, '')
.replace(/\)/g, '');
wktGeometryPart = `MULTIPOINT (${newCoordsFormat})`;
const c = multicoords();
if (!c)
return null;
white();
return {
type: 'MultiPoint',
coordinates: c
};
};
const multilinestring = () => {
if (!$(/^(multilinestring)/i))
return null;
white();
var c = multicoords();
if (!c)
return null;
white();
return {
type: 'MultiLineString',
coordinates: c
};
};
const linestring = () => {
// 选择 WKT 中的 'linestring' 或 'linestring z'(不区分大小写)
if (!$(/^(linestring(\sz)?)/i))
return null;
white();
if (!$(/^(\()/))
return null;
const c = coords();
if (!c)
return null;
if (!$(/^(\))/))
return null;
return {
type: 'LineString',
coordinates: c
};
};
const polygon = () => {
// 选择 WKT 中的 'polygon' 或 'polygon z'(不区分大小写)
if (!$(/^(polygon(\sz)?)/i))
return null;
white();
const c = multicoords();
if (!c)
return null;
return {
type: 'Polygon',
coordinates: c
};
};
const multipolygon = () => {
// 选择 WKT 中的 'multipolygon'(不区分大小写)
if (!$(/^(multipolygon)/i))
return null;
white();
const c = multicoords();
if (!c)
return null;
return {
type: 'MultiPolygon',
coordinates: c
};
};
const geometrycollection = () => {
const geometries = [];
let geometry;
// 选择 WKT 中的 'geometrycollection'(不区分大小写)
if (!$(/^(geometrycollection)/i))
return null;
white();
if (!$(/^(\()/))
return null;
while (geometry = root()) {
geometries.push(geometry);
white();
$(/^(,)/);
white();
}
if (!$(/^(\))/))
return null;
return {
type: 'GeometryCollection',
geometries: geometries
};
};
/**
* 获取整个 GeoJson
* @private
*/
const root = () => {
return point() ||
linestring() ||
polygon() ||
multipoint() ||
multilinestring() ||
multipolygon() ||
geometrycollection();
};
return appendCrs(root());
};
const numberRegexp=/[-+]?([0-9]*\.[0-9]+|[0-9]+)([eE][-+]?[0-9]+)?/,atLeastOneNumberConnectWithSpace=new RegExp(`^${numberRegexp.source}(\\s${numberRegexp.source}){1,}`),pairWKT=coords=>coords.join(" "),ringWKT=ring=>ring.map(pairWKT).join(","),ringsWKT=rings=>rings.map(ringWKT).map(addOutsideBrackets).join(","),multiRingsWKT=multiRings=>multiRings.map(ringsWKT).map(addOutsideBrackets).join(","),addOutsideBrackets=s=>`(${s})`;export const stringify=geojson=>{const target="Feature"===geojson.type?geojson.geometry:geojson;switch(target.type){case"Point":return`POINT (${pairWKT(target.coordinates)})`;case"LineString":return`LINESTRING (${ringWKT(target.coordinates)})`;case"Polygon":return`POLYGON (${ringsWKT(target.coordinates)})`;case"MultiPoint":return`MULTIPOINT (${ringWKT(target.coordinates)})`;case"MultiPolygon":return`MULTIPOLYGON (${multiRings=target.coordinates,multiRings.map(ringsWKT).map(addOutsideBrackets).join(",")})`;case"MultiLineString":return`'MULTILINESTRING (${ringsWKT(target.coordinates)})`;case"GeometryCollection":return`GEOMETRYCOLLECTION (${target.geometries.map(stringify).join(",")})`;default:throw new Error("stringify requires a valid GeoJSON Feature or geometry object as input")}var multiRings};export const parse=wktString=>{const parts=wktString.split(";");let wktGeometryPart=parts.pop();const srid=(parts.shift()||"").split("=").pop();let charOffset=0;const $=reg=>{const match=wktGeometryPart.substring(charOffset).match(reg);return match?(charOffset+=match[0].length,match[0]):null},white=()=>{$(/^\s*/)},multicoords=()=>{white();let depth=0;const rings=[],stack=[rings];let elem,pointer=rings;for(;elem=$(/^(\()/)||$(/^(\))/)||$(/^(,)/)||$(atLeastOneNumberConnectWithSpace);){if("("===elem)stack.push(pointer),pointer=[],stack[stack.length-1].push(pointer),depth++;else if(")"===elem){if(0===pointer.length)return null;if(pointer=stack.pop(),!pointer)return null;if(depth--,0===depth)break}else if(","===elem)pointer=[],stack[stack.length-1].push(pointer);else{if(elem.split(/\s/g).some((v=>isNaN(+v))))return null;Array.prototype.push.apply(pointer,elem.split(/\s/g).map(parseFloat))}white()}return 0!==depth?null:rings},coords=()=>{let item,pt,list=[];for(;pt=$(atLeastOneNumberConnectWithSpace)||$(/^(,)/);)","===pt?(list.push(item),item=[]):pt.split(/\s/g).some((v=>isNaN(+v)))||(item||(item=[]),Array.prototype.push.apply(item,pt.split(/\s/g).map(parseFloat))),white();return item?(list.push(item),list.length?list:null):null},root=()=>(()=>{if(!$(/^(point(\sz)?)/i))return null;if(white(),!$(/^(\()/))return null;const c=coords();return c?(white(),$(/^(\))/)?{type:"Point",coordinates:c[0]}:null):null})()||(()=>{if(!$(/^(linestring(\sz)?)/i))return null;if(white(),!$(/^(\()/))return null;const c=coords();return c&&$(/^(\))/)?{type:"LineString",coordinates:c}:null})()||(()=>{if(!$(/^(polygon(\sz)?)/i))return null;white();const c=multicoords();return c?{type:"Polygon",coordinates:c}:null})()||(()=>{if(!$(/^(multipoint)/i))return null;white();const newCoordsFormat=wktGeometryPart.substring(wktGeometryPart.indexOf("(")+1,wktGeometryPart.length-1).replace(/\(/g,"").replace(/\)/g,"");wktGeometryPart=`MULTIPOINT (${newCoordsFormat})`;const c=multicoords();return c?(white(),{type:"MultiPoint",coordinates:c}):null})()||(()=>{if(!$(/^(multilinestring)/i))return null;white();var c=multicoords();return c?(white(),{type:"MultiLineString",coordinates:c}):null})()||(()=>{if(!$(/^(multipolygon)/i))return null;white();const c=multicoords();return c?{type:"MultiPolygon",coordinates:c}:null})()||(()=>{const geometries=[];let geometry;if(!$(/^(geometrycollection)/i))return null;if(white(),!$(/^(\()/))return null;for(;geometry=root();)geometries.push(geometry),white(),$(/^(,)/),white();return $(/^(\))/)?{type:"GeometryCollection",geometries:geometries}:null})();return(obj=root())&&srid.match(/\d+/)&&(obj.crs={type:"name",properties:{name:`urn:ogc:def:crs:EPSG::${srid}`}}),obj;var obj};
// 数字格式,含可选的正号或负号,支持小数点、e或E表示的科学计数法
const numberRegexp = /[-+]?([0-9]*\.[0-9]+|[0-9]+)([eE][-+]?[0-9]+)?/
// 至少 1 个以上的数字格式,且由空格连接,例如 “1 -2.98 5e-2” atLeastOneNumberConnectWithSpace
const atLeastOneNumberConnectWithSpace = new RegExp(`^${numberRegexp.source}(\\s${numberRegexp.source}){1,}`)
export interface IGeoJson {
type: 'Feature' | 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection',
coordinates?: any[],
geometry?: IGeoJson,
geometries?: IGeoJson[],
crs?: string | {
type: string,
properties: {
name: string
}
},
}
export interface IWKTCoord extends Array<number> { }
export interface IWKTRing extends Array<IWKTCoord> { }
export interface IWKTRings extends Array<IWKTRing> { }
export interface IWKTMultiRings extends Array<any> { }
/**
* @param coords
* @example
* ```
* [112.5, 22.3] => "112.5 22.3"
* ```
*/
const pairWKT = (coords: IWKTCoord) => coords.join(' ')
/**
* @param ring
* @example
* ```
* [[112.52 22.30], [114.31 21.92]] => "112.52 22.30,114.31 21.92"
* ```
*/
const ringWKT = (ring: IWKTRing) => ring.map(pairWKT).join(',')
/**
* @param rings Polygon 或 MultiLineString
* @example
* ```
* [
* [[310, 30], [40, 30], [50, 20]], // ring0
* [[10, 10], [20, 20]] // ring1
* ]
* =>
* "(310 30, 40 30, 50 20),(10 10, 20 20)"
* ```
*/
const ringsWKT = (rings: IWKTRings) => rings.map(ringWKT).map(addOutsideBrackets).join(',')
/**
* @param multiRings MULTIPOLYGON
* @example
* ```
* [
* [ // singlePolygon0
* [[0, 1], [3, 0], [4, 3], [0, 4], [0, 1]],
* ],
* [ // singlePolygon1
* [[3, 4], [6, 3], [5, 5], [3, 4]],
* ],
* ]
* =>
* "((0 1,3 0,4 3,0 4,0 1)),((3 4,6 3,5 5,3 4))"
* ```
*/
const multiRingsWKT = (multiRings: IWKTMultiRings) => multiRings.map(ringsWKT).map(addOutsideBrackets).join(',')
const addOutsideBrackets = (s: string) => `(${s})`
/**
* Stringifies a GeoJSON object into WKT
*/
export const stringify = (geojson: IGeoJson) => {
const target = geojson.type === 'Feature' ? geojson.geometry : geojson
switch (target.type) {
case 'Point':
return `POINT (${pairWKT(target.coordinates as IWKTCoord)})`
case 'LineString':
return `LINESTRING (${ringWKT(target.coordinates)})`
case 'Polygon':
return `POLYGON (${ringsWKT(target.coordinates)})`
case 'MultiPoint':
return `MULTIPOINT (${ringWKT(target.coordinates)})`
case 'MultiPolygon':
return `MULTIPOLYGON (${multiRingsWKT(target.coordinates)})`
case 'MultiLineString':
return `'MULTILINESTRING (${ringsWKT(target.coordinates)})`
case 'GeometryCollection':
return `GEOMETRYCOLLECTION (${target.geometries.map(stringify).join(',')})`
default:
throw new Error('stringify requires a valid GeoJSON Feature or geometry object as input')
}
}
/**
* Parse WKT and return GeoJSON.
*
* @param input A EWKT/WKT geometry
*/
export const parse = (wktString: string) => {
// DEMO: SRID=4269;LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)
const parts = wktString.split(';')
let wktGeometryPart = parts.pop() // 取表示几何的最后一部分
const srid = (parts.shift() || '').split('=').pop() // 取表示坐标系的第一部分
let charOffset = 0
/**
* 使用正则表达式选择 WKT 文本
* @param reg 正则选择器
* @returns 匹配到的字符串
* @private
*/
const $ = (reg: RegExp) => {
const match = wktGeometryPart.substring(charOffset).match(reg)
if (!match) return null
else {
charOffset += match[0].length
return match[0]
}
}
/**
* 为最终的 GeoJson 附加参考坐标系信息
* @private
*/
const appendCrs = (obj: IGeoJson) => {
if (obj && srid.match(/\d+/)) {
obj.crs = {
type: 'name',
properties: {
name: `urn:ogc:def:crs:EPSG::${srid}`
}
}
}
return obj
}
/**
* 移除开头的空格(*号表示0~N个)
* @private
*/
const white = () => { $(/^\s*/) }
const multicoords = () => {
white()
let depth = 0
const rings = []
const stack = [rings]
let pointer = rings
let elem: string
while (elem =
$(/^(\()/) ||
$(/^(\))/) ||
$(/^(,)/) ||
$(atLeastOneNumberConnectWithSpace)
) {
if (elem === '(') {
stack.push(pointer)
pointer = []
stack[stack.length - 1].push(pointer)
depth++
} else if (elem === ')') {
// For the case: Polygon(), ...
if (pointer.length === 0) return null
pointer = stack.pop()
// the stack was empty, input was malformed
if (!pointer) return null
depth--
if (depth === 0) break
} else if (elem === ',') {
pointer = []
stack[stack.length - 1].push(pointer)
} else if (!elem.split(/\s/g).some(v => isNaN(+v))) {
Array.prototype.push.apply(pointer, elem.split(/\s/g).map(parseFloat))
} else {
return null
}
white()
}
if (depth !== 0) return null
return rings
}
const coords = () => {
let list: IWKTCoord[] = []
let item: IWKTCoord
let pt: string
while (pt = $(atLeastOneNumberConnectWithSpace) || $(/^(,)/)) {
if (pt === ',') {
list.push(item)
item = []
} else if (!pt.split(/\s/g).some(v => isNaN(+v))) {
if (!item) item = []
Array.prototype.push.apply(item, pt.split(/\s/g).map(parseFloat))
}
white()
}
if (item) list.push(item)
else return null
return list.length ? list : null
}
const point = (): IGeoJson => {
// 选择 WKT 中的 'point' 或 'point z'(不区分大小写)
if (!$(/^(point(\sz)?)/i)) return null
white()
// 选择 WKT 中的 '('
if (!$(/^(\()/)) return null
// 选择坐标
const c = coords()
if (!c) return null
white()
if (!$(/^(\))/)) return null
return {
type: 'Point',
coordinates: c[0] as []
}
}
const multipoint = (): IGeoJson => {
// 选择 WKT 中的 'multipoint'(不区分大小写)
if (!$(/^(multipoint)/i)) return null
white()
// 重组成新的 MULTIPOINT WKT
const newCoordsFormat = wktGeometryPart
.substring(wktGeometryPart.indexOf('(') + 1, wktGeometryPart.length - 1)
.replace(/\(/g, '')
.replace(/\)/g, '')
wktGeometryPart = `MULTIPOINT (${newCoordsFormat})`
const c = multicoords()
if (!c) return null
white()
return {
type: 'MultiPoint',
coordinates: c
}
}
const multilinestring = (): IGeoJson | null => {
if (!$(/^(multilinestring)/i)) return null
white()
var c = multicoords()
if (!c) return null
white()
return {
type: 'MultiLineString',
coordinates: c
}
}
const linestring = (): IGeoJson => {
// 选择 WKT 中的 'linestring' 或 'linestring z'(不区分大小写)
if (!$(/^(linestring(\sz)?)/i)) return null
white()
if (!$(/^(\()/)) return null
const c = coords()
if (!c) return null
if (!$(/^(\))/)) return null
return {
type: 'LineString',
coordinates: c
}
}
const polygon = (): IGeoJson => {
// 选择 WKT 中的 'polygon' 或 'polygon z'(不区分大小写)
if (!$(/^(polygon(\sz)?)/i)) return null
white()
const c = multicoords()
if (!c) return null
return {
type: 'Polygon',
coordinates: c
}
}
const multipolygon = (): IGeoJson => {
// 选择 WKT 中的 'multipolygon'(不区分大小写)
if (!$(/^(multipolygon)/i)) return null
white()
const c = multicoords()
if (!c) return null
return {
type: 'MultiPolygon',
coordinates: c
}
}
const geometrycollection = (): IGeoJson => {
const geometries: IGeoJson[] = []
let geometry: IGeoJson
// 选择 WKT 中的 'geometrycollection'(不区分大小写)
if (!$(/^(geometrycollection)/i)) return null
white()
if (!$(/^(\()/)) return null
while (geometry = root()) {
geometries.push(geometry)
white()
$(/^(,)/)
white()
}
if (!$(/^(\))/)) return null
return {
type: 'GeometryCollection',
geometries: geometries
}
}
/**
* 获取整个 GeoJson
* @private
*/
const root = () => {
return point() ||
linestring() ||
polygon() ||
multipoint() ||
multilinestring() ||
multipolygon() ||
geometrycollection()
}
return appendCrs(root())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment