UNPKG

wmsc

Version:

WMS-C scheme for Javascript applications

475 lines (444 loc) 12 kB
const convert = require('xml-js') const mercator = require('global-mercator') // Default Values // const MINZOOM = 0 // const MAXZOOM = 20 const SPACES = 2 const BBOX = [-180.0, -85.0511287798, 180.0, 85.0511287798] const BBOX_METERS = [-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428] /** * Get Capabilities * * @param {Options} options Options * @param {string} options.url URL of WMTS service * @param {string} options.title Title of service * @param {string} options.format Format 'png' | 'jpeg' | 'jpg' * @param {number} [options.minzoom=0] Minimum zoom level * @param {number} [options.maxzoom=22] Maximum zoom level * @param {string} [options.accessConstraints] Access Constraints * @param {string} [options.fees] Fees * @param {string} [options.abstract] Abstract * @param {string} [options.identifier] Identifier * @param {string[]} [options.keywords] Keywords * @param {BBox} [options.bbox] BBox [west, south, east, north] * @param {number} [options.spaces=2] Spaces created for XML output * @returns {string} XML string * @example * const xml = wmsc.getCapabilities({ * url: 'http://localhost:5000/WMTS', * title: 'Tile Service XYZ', * identifier: 'service-123', * abstract: '© OSM data', * keyword: ['world', 'imagery', 'wmts'], * format: 'png', * minzoom: 10, * maxzoom: 18, * bbox: [-180, -85, 180, 85] * }) */ function getCapabilities (options) { options = options || {} const spaces = options.spaces || SPACES const json = { _declaration: {_attributes: { version: '1.0', encoding: 'utf-8' }}, WMT_MS_Capabilities: Capabilities(options).WMT_MS_Capabilities } const xml = convert.js2xml(json, { compact: true, spaces: spaces }) return xml } /** * Service Exeception * * @param {string} message * @param {Object} options * @param {number} options.spaces * @return {string} xml */ function exception (message, options) { message = message || 'foo' options = options || {} const spaces = options.spaces || SPACES const json = { _declaration: {_attributes: { version: '1.0', encoding: 'utf-8' }}, ServiceExceptionReport: { _attributes: {version: '1.1.1'}, ServiceException: { _text: message } } } const xml = convert.js2xml(json, { compact: true, spaces: spaces }) return xml } /** * Capabilities JSON scheme * * @private * @param {Options} options Options * @param {string} options.url URL of WMTS service * @returns {ElementCompact} JSON scheme * @example * Capabilities({ * url: 'http://localhost:5000' * }) */ function Capabilities (options) { options = options || {} // Required options const url = normalize(options.url) if (!url) throw new Error('<url> is required') return { WMT_MS_Capabilities: Object.assign({ _attributes: { version: '1.1.0' } }, Service(options), Capability(options) ) } } /** * Service JSON scheme * * @private * @param {Options} options Options * @param {string} options.title Title * @param {string} options.url URL * @param {string} options.format Format 'png' | 'jpeg' | 'jpg' * @param {string} [options.abstract] Abstract * @param {string} [options.identifier] Identifier * @param {BBox} [options.bbox=[-180, -85, 180, 85]] BBox [west, south, east, north] * @returns {ElementCompact} JSON scheme * @example * Service({ * title: 'Service name', * abstract: 'A long description of this service', * keywords: ['world', 'wmts', 'imagery'] * }) */ function Service (options) { options = options || {} // Required options const title = options.title const url = options.url if (!title) throw new Error('options.title is required') if (!url) throw new Error('options.url is required') // Optional options const abstract = options.abstract const accessConstraints = options.accessConstraints || 'none' const fees = options.fees || 'none' const keywords = options.keywords return clean({ 'Service': { 'Name': {_text: 'OGC:WMS'}, 'Title': {_text: title}, 'Abstract': {_text: abstract}, 'KeywordList': (keywords) ? Keywords(keywords)['KeywordList'] : undefined, 'OnlineResource': { _attributes: { 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xlink:type': 'simple', 'xlink:href': url }}, // ContactAddress: ContactAddress(options).ContactAddress, 'Fees': {_text: fees}, 'AccessConstraints': {_text: accessConstraints} } }) } /** * Keywords JSON scheme * * @private * @param {string[]} [keywords] * @returns {ElementCompact} JSON scheme * @example * Keywords(['world', 'imagery', 'wmts']) */ function Keywords (keywords) { keywords = keywords || [] return { 'KeywordList': { 'Keyword': keywords.map(function (keyword) { return {_text: String(keyword)} }) } } } /** * Capabilities.Capability JSON scheme * * @private * @param {Options} options Options * @returns {Element} * @example * Capability() * //= Capability > [Request, Exception, VendorSpecificCapabilities, UserDefinedSymbolization, Layer] */ function Capability (options) { options = options || {} return { Capability: { Request: Request(options).Request, Exception: { Format: [ {_text: 'application/vnd.ogc.se_xml'}, {_text: 'application/vnd.ogc.se_inimage'}, {_text: 'application/vnd.ogc.se_blank'} ] }, VendorSpecificCapabilities: TileSet(options), Layer: Layer(options).Layer } } } /** * TileSet JSON scheme * * @private * @param {number} options.minzoom * @param {number} options.maxzoom * @param {string} options.format * @param {string} options.identifier * @param {[number, number, number, number]} options.bbox [west, south, east, north] * @returns {ElementCompact} JSON scheme */ function TileSet (options) { options = options || {} if (!options.format) throw new Error('options.format is required') if (options.identifier === undefined) throw new Error('options.identifier is required') if (options.minzoom === undefined) throw new Error('options.minzoom is required') if (options.maxzoom === undefined) throw new Error('options.maxzoom is required') /** WARNING: Removed user input BBOX */ // const bboxMeters = mercator.bboxToMeters(options.bbox) || BBOX_METERS const format = (options.format === 'jpg') ? 'jpeg' : options.format return { TileSet: { SRS: {_text: 'EPSG:3857'}, BoundingBox: BoundingBox(BBOX_METERS, 'EPSG:3857'), Resolutions: { _text: resolutions(options.minzoom, options.maxzoom).join(' ') }, Width: {_text: '256'}, Height: {_text: '256'}, Format: {_text: 'image/' + format}, Layers: {_text: options.identifier} } } } /** * Resolutions - Zoom Scales * * @private * @param {number} minzoom * @param {number} maxzoom * @returns {Array<number>} */ function resolutions (minzoom, maxzoom) { return range(minzoom, maxzoom + 1).map(function (zoom) { return mercator.resolution(zoom) }) } /** * Request JSON scheme * * @private * @param {string} options.url * @param {string} options.format * @returns {ElementCompact} JSON scheme */ function Request (options) { if (!options.url) throw new Error('options.url is required') if (!options.format) throw new Error('options.format is required') const url = options.url const format = (options.format === 'jpg') ? 'jpeg' : options.format return { Request: { GetCapabilities: { Format: {_text: 'application/vnd.ogc.wms_xml'}, DCPType: DCPType(url) }, GetMap: { Format: {_text: 'image/' + format}, DCPType: DCPType(url) }, /** WARNING: Might need to remove GetFeatureInfo */ GetFeatureInfo: { Format: [ {_text: 'application/vnd.ogc.gml'}, {_text: 'text/plain'}, {_text: 'text/html'} ], DCPType: DCPType(url) } } } } /** * DCPType JSON scheme * * @private * @param {string} url * @returns {ElementCompact} JSON scheme */ function DCPType (url) { if (!url) throw new Error('url is required') return { HTTP: { Get: { OnlineResource: { _attributes: { 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xlink:type': 'simple', 'xlink:href': url } } } } } } /** * Capabilities.Contents.Layer JSON scheme * * @private * @param {Options} options * @param {string} options.title * @param {string} options.url * @param {string} options.identifier * @param {BBox} [options.bbox] (west, south, east, north) * @returns {ElementCompact} JSON scheme * @example * Layer({ * title: 'Tile Service' * url: 'http://localhost:5000/wmts' * format: 'jpg' * }) */ function Layer (options) { options = options || {} if (options.identifier === undefined) throw new Error('identifier is required') if (options.title === undefined) throw new Error('title is required') if (options.url === undefined) throw new Error('url is required') /** WARNING: removed user input BBox */ // const bbox = options.bbox || BBOX // const bboxMeters = mercator.bboxToMeters(bbox) || BBOX_METERS const BoundingBoxes = [ BoundingBox(BBOX_METERS, 'EPSG:900913'), BoundingBox(BBOX, 'EPSG:4326'), BoundingBox(BBOX_METERS, 'EPSG:3857') ] return { Layer: { Title: {_text: options.title}, /** WARNING: might need to drop WGS84 */ SRS: [ {_text: 'EPSG:900913'}, {_text: 'EPSG:4326'}, {_text: 'CRS:84'}, {_text: 'EPSG:3857'} ], LatLonBoundingBox: BoundingBox(BBOX), BoundingBox: BoundingBoxes, // Sub-Layer Layer: { Name: {_text: options.identifier}, Title: {_text: options.title}, LatLonBoundingBox: BoundingBox(BBOX), BoundingBox: BoundingBoxes } } } } /** * BBox JSON scheme * * @private * @param {number[]} bbox * @param {string} [srs] * @returns {ElementCompact} JSON scheme */ function BoundingBox (bbox, srs) { if (!bbox) throw new Error('bbox is required') const west = bbox[0] const south = bbox[1] const east = bbox[2] const north = bbox[3] return clean({ _attributes: { SRS: srs, minx: west, miny: south, maxx: east, maxy: north } }) } /** * Clean remove undefined attributes from object * * @private * @param {Object} obj JSON object * @returns {Object} clean JSON object * @example * clean({foo: undefined, bar: 123}) * //={bar: 123} * clean({foo: 0, bar: 'a'}) * //={foo: 0, bar: 'a'} */ function clean (obj) { return JSON.parse(JSON.stringify(obj)) } /** * Normalize URL * * @private * @param {string} url * @returns {string} Normalized URL * @example * normalize('http://localhost:5000') * //=http://localhost:5000/ */ function normalize (url) { return url && url.replace(/$\//, '') } /** * Generate an integer Array containing an arithmetic progression. * * @private * @param {number} [start=0] Start * @param {number} stop Stop * @param {number} [step=1] Step * @returns {number[]} range * @example * mercator.range(3) * //=[ 0, 1, 2 ] * mercator.range(3, 6) * //=[ 3, 4, 5 ] * mercator.range(6, 3, -1) * //=[ 6, 5, 4 ] */ function range (start, stop, step) { if (stop == null) { stop = start || 0 start = 0 } if (!step) { step = stop < start ? -1 : 1 } var length = Math.max(Math.ceil((stop - start) / step), 0) var range = Array(length) for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start } return range } module.exports = { getCapabilities: getCapabilities, exception: exception, Capabilities: Capabilities, Capability: Capability, Service: Service, Keywords: Keywords, Request: Request, TileSet: TileSet, Layer: Layer, resolutions: resolutions, range: range, clean: clean }