diff --git a/src/App.svelte b/src/App.svelte index d0abac7..dede907 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -9,8 +9,8 @@ import file_lib from "./libs/file"; import Modal from "./components/Modal.svelte"; - import MapApp from './services/map' - import SvgMapParser from './svgmap/SvgMapParser' + + import SvgMapParser from './svgmap/SvgMap.js' import get_bbox from './libs/bbox' diff --git a/src/back_.js b/src/back_.js index 6be1df5..6bfc6c1 100644 --- a/src/back_.js +++ b/src/back_.js @@ -1,26 +1,33 @@ import fs, { symlinkSync } from "fs"; import axios from "axios"; -import SvgMapParser from "./svgmap/SvgMapParser.js"; +import SvgMap from "./svgmap/SvgMap.js"; +import SvgMapBuilder from "./svgmap/SvgMapBuilder.js"; +import CoordSystem from "./libs/CoordSystem.js"; +import SvgNode from "./svgmap/SvgNode.js"; import SvgNodes from "./svgmap/SvgNodes.js"; +import Transfrom from "./libs/Transform.js"; +import BBox from "./libs/bbox.js"; -import math_lib from './libs/math.js' +// import SvgNodes from "./svgmap/SvgNodes.js"; + +// import math_lib from './libs/math.js' const back_url = "http://localhost:4000"; -const path = "../../data/2022-03-16"; +const path = "../../data/prodmaps"; // let file = fs.readFileSync('../data/2022-03-16/49_Кар тек и сум отб 7 об_вост.svg') // let file = fs.readFileSync('../data/2022-03-16/49_Кар тек и сум отб 7 об_вост-sm.svg') // let file = fs.readFileSync('../data/2022-03-16/49_Кар тек и сум отб 7 об_вост-inch.svg') // let file = fs.readFileSync(`${path}/49_Кар тек и сум отб 7 об_вост-mm.svg`) -let file = fs.readFileSync(`D:/dev/git/kz/prodmaps/tmp/49_Кар тек и сум отб 7 об_вост.svg`); +let file = fs.readFileSync(`${path}/49_Кар тек и сум отб 7 об_вост.svg`); // let file = fs.readFileSync('../data/2022-03-16/49_Кар тек и сум отб 7 об_вост-px.svg') let styles = JSON.parse(fs.readFileSync("./moc/styles.json").toString()); -let parser = new SvgMapParser(); +let parser = new SvgMap(); function build_scaler() { // Получить функцию для перевода координат из геологических в локальные. @@ -51,64 +58,59 @@ async function start() { let ww1 = wells.filter((x) => x.well == anchor.w1.name)[0]; let ww2 = wells.filter((x) => x.well == anchor.w2.name)[0]; - console.log(ww1, ww2); - if (!ww1) return ui.modal.show(`Скважины ${anchor.w1.name} не найдено в базе.`, ["ok"]); if (!ww2) return ui.modal.show(`Скважины ${anchor.w2.name} не найдено в базе.`, ["ok"]); - anchor.w1.whx = ww1.whx; - anchor.w1.why = ww1.why; - anchor.w2.whx = ww2.whx; - anchor.w2.why = ww2.why; + // function cs(x0, y0, x1, y1) { + // return { x0, y0, x1, y1 }; + // } - function dist(x1, y1, x2, y2) { - let dx = x2 - x1, - dy = y2 - y1; - return Math.sqrt(dx * dx + dy * dy); - } + // function build_tr(cs1, cs2) { + // let dx1 = cs1.x1 - cs1.x0 + // let dy1 = cs1.y1 - cs1.y0 + // let dx2 = cs2.x1 - cs2.x0 + // let dy2 = cs2.y1 - cs2.y0 - function cs(x0, y0, x1, y1) { - return { x0, y0, x1, y1 }; - } + // const kx12 = dx2 / dx1 + // const ky12 = dy2 / dy1; + // const kx21 = dx1 / dx2 + // const ky21 = dy1 / dy2; - function build_tr(cs1, cs2) { - let dx1 = cs1.x1 - cs1.x0 - let dy1 = cs1.y1 - cs1.y0 - let dx2 = cs2.x1 - cs2.x0 - let dy2 = cs2.y1 - cs2.y0 + // let tr = { + // tr12: { + // trx(x) { + // return (x - cs1.x0) * kx12 + cs2.x0; + // }, + // try(y) { + // return (y - cs1.y0) * ky12 + cs2.y0; + // }, + // }, + // tr21: { + // trx(x) { + // return (x - cs2.x0) * kx21 + cs1.x0; + // }, + // try(y) { + // return (y - cs2.y0) * ky21 + cs1.y0; + // }, + // }, + // }; + + // return tr; + // } - const kx12 = dx2 / dx1 - const ky12 = dy2 / dy1; - const kx21 = dx1 / dx2 - const ky21 = dy1 / dy2; - let tr = { - tr12: { - trx(x) { - return (x - cs1.x0) * kx12 + cs2.x0; - }, - try(y) { - return (y - cs1.y0) * ky12 + cs2.y0; - }, - }, - tr21: { - trx(x) { - return (x - cs2.x0) * kx21 + cs1.x0; - }, - try(y) { - return (y - cs2.y0) * ky21 + cs1.y0; - }, - }, - }; - return tr; - } // Функция перевода координат из глобальных в локальные. - const cs1 = cs(anchor.w1.x, anchor.w1.y, anchor.w2.x, anchor.w2.y); - const cs2 = cs(anchor.w1.whx, anchor.w1.why, anchor.w2.whx, anchor.w2.why); - let tr = build_tr(cs1, cs2); - console.log(tr); + const cs1 = new CoordSystem(anchor.w1.x, anchor.w1.y, anchor.w2.x, anchor.w2.y); + const cs2 = new CoordSystem(ww1.whx, ww1.why, ww2.whx, ww2.why); + // let tr = Transfrom.fromCoordSyses(cs2, cs1); + + // let tr = Transfrom.fromCoordSyses(cs2, cs1); + + // console.log('tr is:', tr); + + // console.log(wells) @@ -134,47 +136,436 @@ async function start() { `// WHERE well='${anchor.w1.name}'`; - // const query_all = `SELECT well, whx, why FROM wells`// WHERE well='${anchor.w1.name}'`; - // const query_all = `SELECT well, whx, why FROM wells WHERE well='${anchor.w1.name}' OR well='${anchor.w2.name}'`; + // Получим максимально удаленные скважины не лежащие вдоль оси. let all_wells = await axios.post(`${back_url}/proxy/sqlite`, { query: query_all }).then((x) => x.data.data); - console.log(all_wells.map(x => x.wlf)) //.slice(0, 1)) - let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.tr21.trx(x.whx), ly: tr.tr21.try(x.why) })); + let bbox = BBox.from_array(all_wells.map(w => ({x: w.whx, y: w.why}))) + console.log('BBOX', bbox) + console.log('BBOX', bbox.toLTWH()) + console.log('BBOX', bbox.toLTRB()) - - // let svg = SvgNodes.svg().set_attrs({ - // width: sc.w + "mm", - // height: sc.h + "mm", - // viewBox: `0 0 ${sc.w * sc.k} ${sc.h * sc.k}`, - // }); - let svg = SvgNodes.group(['']).set_attr("id", "WLPT") + tr = Transfrom.fromCoordSyses() - const tons_in_cm2 = 10000 - function t2r(tons){ - // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) - return Math.sqrt(tons / tons_in_cm2 * 100 / Math.PI) - } - console.log(mapped_wells.filter(x => x.well == '213').map(x => ({...x, ttt: t2r(x.wlpt)}))) + let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + // const tons_in_cm2 = 10000 + + // function t2r(tons){ + // // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + // return Math.sqrt(tons / tons_in_cm2 * 100 / Math.PI) + // } + + // console.log(mapped_wells.filter(x => x.well == '213').map(x => ({...x, ttt: t2r(x.wlpt)}))) // console.log(math_lib.make_ranges(vals)) // svg.append(mapped_wells.map((x) => SvgNodes.ring_sector(1.5 * sc.k, 6 * sc.k, 0, 120).move(x.lx, x.ly).set_style(styles.gpt))); - // Круги - let g_wopts = SvgNodes.group() - svg.append(g_wopts) - g_wopts.append(mapped_wells.map((x) => SvgNodes.ring_sectors(1.5 * sc.k, t2r(x.wlpt) * sc.k, [{v: x.wopt, style: styles.opt}, {v: x.wwpt, style: styles.wpt}]).move(x.lx, x.ly))); + - svg.append(mapped_wells.map((x) => SvgNodes.circle(1.5 * sc.k).move(x.lx, x.ly).set_style(styles.wellhead))); - svg.append(mapped_wells.map((x) => SvgNodes.text(x.well).move(x.lx + 1.7 * sc.k, x.ly).set_style(styles.wellhead))); - svg.append(mapped_wells.map((x) => SvgNodes.text(`${Math.round(x.wlf * 1000)/10 || ''}%`).move(x.lx + 1.7 * sc.k, x.ly + 1 * sc.k).set_style(styles.wlf))); + const settings = { + tons_in_cm2: 10000, + styles + } - let data = file.toString().replace('', svg.render() + '') + { + let ti_layer = parser.build_tp_layer(mapped_wells, settings) + let svg = SvgNodes.svg().set_attr("width", "1000px").set_attr("height", "1000px").set_attr("viewBox", "0 0 1000 1000") + // let defs = new SvgNode('defs') + // defs.append(ti_layer.defs) + // console.log(ti_layer.defs.tag) + svg.append(ti_layer.defs) + svg.append(ti_layer.svg) + fs.writeFileSync(`${path}/out.svg`, svg.render()); + } + + + // let ti_layer = parser.build_tp_layer(mapped_wells, settings) + // let data = file.toString() + // .replace('', ti_layer.defs.render() + '') + // .replace('', ti_layer.svg.render() + '') + // fs.writeFileSync("../tmp/out_prod.svg", data) //svg.render()); + + + // let ti_layer_inj = parser.build_ti_layer(mapped_wells, settings) + // let data2 = file.toString() + // .replace('', ti_layer_inj.defs.render() + '') + // .replace('', ti_layer_inj.svg.render() + '') + // fs.writeFileSync("../tmp/out_inj.svg", data2) //svg.render()); - fs.writeFileSync("../tmp/out.svg", data) //svg.render()); } -start(); + +async function build_map_pt(devobj) { + // const DATE = '2023-01-01' + + const query_all = `SELECT + wells.well, wells.whx, wells.why, + MAX(production_injections.date) as date + ,SUM(production_injections.woptm) as wopt + ,SUM(production_injections.wwptm) as wwpt + ,SUM(production_injections.wsgptv) as wgpt + ,SUM(production_injections.wwitv) as wwit + ,SUM(production_injections.wwptm)+SUM(production_injections.woptm) as wlpt + ,SUM(production_injections.wwptm)/(SUM(production_injections.wwptm)+SUM(production_injections.woptm)) as wlf + FROM + wells, production_injections + WHERE + wells.well=production_injections.well + + AND object='${devobj}' + GROUP BY + wells.well + + ` + + let all_wells = await axios.post(`${back_url}/proxy/sqlite`, { query: query_all }).then((x) => x.data.data); + if (all_wells.length == 0){ + // fs.writeFileSync(`${path}/out_devobj-${devobj}.svg`, ""); + return + } + + let bbox = BBox.from_array(all_wells.map(w => ({x: w.whx, y: w.why}))) + + { + const builder = new SvgMapBuilder() + + const settings = { + tons_in_cm2: 10000, + ppu: 100, // 100 + styles: builder.update_styles(styles, 100), + // Масштаб 1мм карты / 1мм реальный (1000мм в 1м) + map_scale: 1 / 10000 * 1000 + } + + // Функция перевода координат из глобальных в локальные. + const cs1 = new CoordSystem(bbox.l, bbox.t, bbox.r, bbox.b) + const cs_mm = cs1.clone().flipy().moveto(0, 0).scale(settings.map_scale) + const cs_ppu = cs_mm.clone().scale(settings.ppu) + let tr = Transfrom.fromCoordSyses(cs1, cs_ppu); + + let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + let ti_layer = builder.build_tp_layer(mapped_wells, settings) + const csw = Math.abs(cs_mm.x1 - cs_mm.x0) + const csh = Math.abs(cs_mm.y1 - cs_mm.y0) + const bbox_ppu = BBox.fromLTRB(cs_ppu.x0, cs_ppu.y0, cs_ppu.x1, cs_ppu.y1) + + let svg = SvgNodes.svg() + .set_attr("width", csw + "mm").set_attr("height", csh + "mm") + .set_attr("viewBox", `${bbox_ppu.l} ${bbox_ppu.t} ${bbox_ppu.w()} ${bbox_ppu.h()}`) + svg.append(ti_layer.defs) + svg.append(ti_layer.svg) + fs.writeFileSync(`${path}/out_devobj-${devobj}-PT.svg`, svg.render()); + } +} + +async function build_map_it(devobj) { + // const DATE = '2023-01-01' + + const query_all = `SELECT + wells.well, wells.whx, wells.why, + MAX(production_injections.date) as date + ,SUM(production_injections.woptm) as wopt + ,SUM(production_injections.wwptm) as wwpt + ,SUM(production_injections.wsgptv) as wgpt + ,SUM(production_injections.wwitv) as wwit + ,SUM(production_injections.wwptm)+SUM(production_injections.woptm) as wlpt + ,SUM(production_injections.wwptm)/(SUM(production_injections.wwptm)+SUM(production_injections.woptm)) as wlf + FROM + wells, production_injections + WHERE + wells.well=production_injections.well + AND object='${devobj}' + GROUP BY + wells.well + HAVING + wwit>0 + + ` + + let all_wells = await axios.post(`${back_url}/proxy/sqlite`, { query: query_all }).then((x) => x.data.data); + if (all_wells.length == 0){ + // fs.writeFileSync(`${path}/out_devobj-${devobj}.svg`, ""); + return + } + + let bbox = BBox.from_array(all_wells.map(w => ({x: w.whx, y: w.why}))) + + { + const builder = new SvgMapBuilder() + + const settings = { + tons_in_cm2: 10000, + ppu: 100, // 100 + styles: builder.update_styles(styles, 100), + // Масштаб 1мм карты / 1мм реальный (1000мм в 1м) + map_scale: 1 / 10000 * 1000 + } + + // Функция перевода координат из глобальных в локальные. + const cs1 = new CoordSystem(bbox.l, bbox.t, bbox.r, bbox.b) + const cs_mm = cs1.clone().flipy().moveto(0, 0).scale(settings.map_scale) + const cs_ppu = cs_mm.clone().scale(settings.ppu) + let tr = Transfrom.fromCoordSyses(cs1, cs_ppu); + + let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + let ti_layer = builder.build_ti_layer(mapped_wells, settings) + const csw = Math.abs(cs_mm.x1 - cs_mm.x0) + const csh = Math.abs(cs_mm.y1 - cs_mm.y0) + const bbox_ppu = BBox.fromLTRB(cs_ppu.x0, cs_ppu.y0, cs_ppu.x1, cs_ppu.y1) + + let svg = SvgNodes.svg() + .set_attr("width", csw + "mm").set_attr("height", csh + "mm") + .set_attr("viewBox", `${bbox_ppu.l} ${bbox_ppu.t} ${bbox_ppu.w()} ${bbox_ppu.h()}`) + svg.append(ti_layer.defs) + svg.append(ti_layer.svg) + fs.writeFileSync(`${path}/out_devobj-${devobj}-IT.svg`, svg.render()); + } + + + // let ti_layer = parser.build_tp_layer(mapped_wells, settings) + // let data = file.toString() + // .replace('', ti_layer.defs.render() + '') + // .replace('', ti_layer.svg.render() + '') + // fs.writeFileSync("../tmp/out_prod.svg", data) //svg.render()); + + + // let ti_layer_inj = parser.build_ti_layer(mapped_wells, settings) + // let data2 = file.toString() + // .replace('', ti_layer_inj.defs.render() + '') + // .replace('', ti_layer_inj.svg.render() + '') + // fs.writeFileSync("../tmp/out_inj.svg", data2) //svg.render()); +} + +async function build_map_pty(devobj) { + let sql_max_year = `SELECT strftime('%Y', max(date)) as max_year FROM production_injections` + const max_year = await axios.post(`${back_url}/proxy/sqlite`, { query: sql_max_year }).then((x) => x.data.data[0].max_year); + + // const DATE = '2023-01-01' + + const query_all = `SELECT + wells.well, wells.whx, wells.why, + MAX(production_injections.date) as date + ,SUM(production_injections.woptm) as wopt + ,SUM(production_injections.wwptm) as wwpt + ,SUM(production_injections.wsgptv) as wgpt + ,SUM(production_injections.wwitv) as wwit + ,SUM(production_injections.wwptm)+SUM(production_injections.woptm) as wlpt + ,SUM(production_injections.wwptm)/(SUM(production_injections.wwptm)+SUM(production_injections.woptm)) as wlf + FROM + wells, production_injections + WHERE + wells.well=production_injections.well + AND object='${devobj}' + AND strftime('%Y', date)='${max_year}' + GROUP BY + wells.well + + ` + + let all_wells = await axios.post(`${back_url}/proxy/sqlite`, { query: query_all }).then((x) => x.data.data); + if (all_wells.length == 0){ + // fs.writeFileSync(`${path}/out_devobj-${devobj}-CP.svg`, ""); + return + } + + let bbox = BBox.from_array(all_wells.map(w => ({x: w.whx, y: w.why}))) + + { + const builder = new SvgMapBuilder() + + const settings = { + tons_in_cm2: 10000, + ppu: 100, // 100 + styles: builder.update_styles(styles, 100), + // Масштаб 1мм карты / 1мм реальный (1000мм в 1м) + map_scale: 1 / 10000 * 1000 + } + + // Функция перевода координат из глобальных в локальные. + const cs1 = new CoordSystem(bbox.l, bbox.t, bbox.r, bbox.b) + const cs_mm = cs1.clone().flipy().moveto(0, 0).scale(settings.map_scale) + const cs_ppu = cs_mm.clone().scale(settings.ppu) + let tr = Transfrom.fromCoordSyses(cs1, cs_ppu); + + let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + let ti_layer = builder.build_tp_layer(mapped_wells, settings) + const csw = Math.abs(cs_mm.x1 - cs_mm.x0) + const csh = Math.abs(cs_mm.y1 - cs_mm.y0) + const bbox_ppu = BBox.fromLTRB(cs_ppu.x0, cs_ppu.y0, cs_ppu.x1, cs_ppu.y1) + + let svg = SvgNodes.svg() + .set_attr("width", csw + "mm").set_attr("height", csh + "mm") + .set_attr("viewBox", `${bbox_ppu.l} ${bbox_ppu.t} ${bbox_ppu.w()} ${bbox_ppu.h()}`) + svg.append(ti_layer.defs) + svg.append(ti_layer.svg) + fs.writeFileSync(`${path}/out_devobj-${devobj}-PC.svg`, svg.render()); + } +} + + + +async function start2() { + const DATE = '2023-01-01' + + const sql_objects = `SELECT distinct(object) as devobj FROM production_injections` + + let objects = await axios.post(`${back_url}/proxy/sqlite`, { query: sql_objects }).then((x) => x.data.data.map(y => y.devobj)); + + objects.map(build_map_pt) + objects.map(build_map_it) + objects.map(build_map_pty) + + return + + const query_all = `SELECT + wells.well, wells.whx, wells.why, + MAX(production_injections.date) as date + ,SUM(production_injections.woptm) as wopt + ,SUM(production_injections.wwptm) as wwpt + ,SUM(production_injections.wsgptv) as wgpt + ,SUM(production_injections.wwitv) as wwit + ,SUM(production_injections.wwptm)+SUM(production_injections.woptm) as wlpt + ,SUM(production_injections.wwptm)/(SUM(production_injections.wwptm)+SUM(production_injections.woptm)) as wlf + FROM + wells, production_injections + WHERE + wells.well=production_injections.well + AND production_injections.date<='${DATE}' + AND object='8' + GROUP BY + wells.well + + `// WHERE well='${anchor.w1.name}'`; + + // const query_all2 = `SELECT distinct(object) FROM production_injections limit 10` + + let all_wells = await axios.post(`${back_url}/proxy/sqlite`, { query: query_all }).then((x) => x.data.data); + + // console.log(all_wells) + // return + + let bbox = BBox.from_array(all_wells.map(w => ({x: w.whx, y: w.why}))) + + // let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + // let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + // const tons_in_cm2 = 10000 + + // function t2r(tons){ + // // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + // return Math.sqrt(tons / tons_in_cm2 * 100 / Math.PI) + // } + + // console.log(mapped_wells.filter(x => x.well == '213').map(x => ({...x, ttt: t2r(x.wlpt)}))) + + // console.log(math_lib.make_ranges(vals)) + + // svg.append(mapped_wells.map((x) => SvgNodes.ring_sector(1.5 * sc.k, 6 * sc.k, 0, 120).move(x.lx, x.ly).set_style(styles.gpt))); + + + + + { + const builder = new SvgMapBuilder() + + const settings = { + tons_in_cm2: 10000, + ppu: 100, // 100 + styles: builder.update_styles(styles, 100), + // Масштаб 1мм карты / 1мм реальный (1000мм в 1м) + map_scale: 1 / 10000 * 1000 + } + + +console.log(settings) + + function mm2ppu(v){ + if (v.endsWith('mm')){ + return parseFloat(v.slice(0, v.length - 2)) * settings.ppu * 100 / 69 + } + } + + function pt2ppu(v){ + if (v.endsWith('pt')){ + return parseFloat(v.slice(0, v.length - 2)) * settings.ppu * 100 / 283.46 + } + } + + function u2ppu(v){ + if (v.endsWith('mm')){ + return parseFloat(v.slice(0, v.length - 2)) * settings.ppu * 100 / 69 + } + if (v.endsWith('pt')){ + return parseFloat(v.slice(0, v.length - 2)) * settings.ppu * 100 / 283.46 + } + } + + // mm = 25.4 * (px / 96) + + // settings.styles.wellhead["font-family"] = "Courier New", + // console.log(mm2ppu(settings.styles.wellhead['font-size'])) + // settings.styles.wellhead['font-size'] = u2ppu('10pt') //settings.styles.wellhead['font-size']) + + // const query = `SELECT well, whx, why FROM wells WHERE well='203'`; + const query = `SELECT well, whx, why FROM wells WHERE well='R-2'`; + let wells = await axios.post(`${back_url}/proxy/sqlite`, { query }).then((x) => x.data.data); + console.log(wells); + let c = [wells[0].whx - bbox.l, wells[0].why - bbox.t] + console.log(c) + + + + // Функция перевода координат из глобальных в локальные. + const cs1 = new CoordSystem(bbox.l, bbox.t, bbox.r, bbox.b) + const cs_mm = cs1.clone().moveto(0, 0).scale(settings.map_scale) + const cs_ppu = cs_mm.clone().scale(settings.ppu) + let tr = Transfrom.fromCoordSyses(cs1, cs_ppu); + + console.log('cs_mm', cs_mm, 'cs_ppu', cs_ppu) + + + + + let mapped_wells = all_wells.map((x) => ({ ...x, lx: tr.trx(x.whx), ly: tr.try(x.why) })); + + let ti_layer = builder.build_tp_layer(mapped_wells, settings) + const csw = (cs_mm.x1 - cs_mm.x0) + const csh = (cs_mm.y1 - cs_mm.y0) + + let svg = SvgNodes.svg().set_attr("width", csw + "mm").set_attr("height", csh + "mm") + // .set_attr("viewBox", "0 0 1000 1000") + .set_attr("viewBox", `${cs_ppu.x0} ${cs_ppu.y0} ${(cs_ppu.x1 - cs_ppu.x0)} ${(cs_ppu.y1 - cs_ppu.y0)}`) + // let defs = new SvgNode('defs') + // defs.append(ti_layer.defs) + // console.log(ti_layer.defs.tag) + svg.append(ti_layer.defs) + svg.append(ti_layer.svg) + fs.writeFileSync(`${path}/out.svg`, svg.render()); + } + + + // let ti_layer = parser.build_tp_layer(mapped_wells, settings) + // let data = file.toString() + // .replace('', ti_layer.defs.render() + '') + // .replace('', ti_layer.svg.render() + '') + // fs.writeFileSync("../tmp/out_prod.svg", data) //svg.render()); + + + // let ti_layer_inj = parser.build_ti_layer(mapped_wells, settings) + // let data2 = file.toString() + // .replace('', ti_layer_inj.defs.render() + '') + // .replace('', ti_layer_inj.svg.render() + '') + // fs.writeFileSync("../tmp/out_inj.svg", data2) //svg.render()); + +} + + +start2(); diff --git a/src/libs/CoordSystem.js b/src/libs/CoordSystem.js new file mode 100644 index 0000000..909fa91 --- /dev/null +++ b/src/libs/CoordSystem.js @@ -0,0 +1,54 @@ +export default class CoordSystem{ + /** + * Создает ненормированную систему координат с началом в x0,y0 и направлением x1,y1 + * @param {*} x0 + * @param {*} y0 + * @param {*} x1 + * @param {*} y1 + * @returns + */ + constructor(x0, y0, x1, y1) { + this.x0 = x0 + this.y0 = y0 + this.x1 = x1 + this.y1 = y1 + } + + scale(k){ + this.x1 *= k + this.y1 *= k + return this + } + + move(dx, dy){ + this.x0 += dx + this.y0 += dy + this.x1 += dx + this.y1 += dy + return this + } + + moveto(x, y){ + let dx = x - this.x0 + let dy = y - this.y0 + return this.move(dx, dy) + } + + flipx(){ + let x = this.x0 + this.x0 = this.x1 + this.x1 = x + // this.x0 += 500 + // this.x1 = 2 * this.x0 - this.x1 + 500 + return this + } + + flipy(){ + this.y1 = 2 * this.y0 - this.y1 + return this + } + + clone(){ + return new CoordSystem(this.x0, this.y0, this.x1, this.y1) + } +}; diff --git a/src/libs/Transform.js b/src/libs/Transform.js new file mode 100644 index 0000000..d774bab --- /dev/null +++ b/src/libs/Transform.js @@ -0,0 +1,30 @@ +export default class Transfrom { + static fromCoordSyses(cs1, cs2){ + let tr = new Transfrom() + + tr.cs1 = cs1 + tr.cs2 = cs2 + + let dx1 = cs1.x1 - cs1.x0; + let dy1 = cs1.y1 - cs1.y0; + let dx2 = cs2.x1 - cs2.x0; + let dy2 = cs2.y1 - cs2.y0; + + tr.kx12 = dx2 / dx1; + tr.ky12 = dy2 / dy1; + + return tr + } + + trx(x){ + return (x - this.cs1.x0) * this.kx12 + this.cs2.x0; + } + + try(y){ + return (y - this.cs1.y0) * this.ky12 + this.cs2.y0; + } + + trp(p){ + return {x: this.trx(p.x), y: this.trx(p.y)} + } +} diff --git a/src/libs/bbox.js b/src/libs/bbox.js index 67ebd04..3c2a9e6 100644 --- a/src/libs/bbox.js +++ b/src/libs/bbox.js @@ -1,11 +1,95 @@ -module.exports = function (data) { - return data.reduce( - (s, c) => ({ - l: Math.min(s.l, c.x), - t: Math.min(s.t, c.y), - r: Math.max(s.r, c.x), - b: Math.max(s.b, c.y), - }), - { l: data[0].x, t: data[0].y, r: data[0].x, b: data[0].y } - ); -}; +export default class BBox { + static fromLTWH(l, t, w, h){ + return BBox.fromLTRB(l, t, l + w, t + h) + } + + static fromLTRB(l, t, r, b){ + return Object.assign(new BBox(), {l: Math.min(l, r), t: Math.min(t, b), r: Math.max(l, r), b: Math.max(t, b)}) + } + + static from_array(arr_xy){ + const bbox = BBox.fromLTWH(arr_xy[0].x, arr_xy[0].y, 0, 0) + return bbox.append_many(arr_xy) + } + + w(){ + return this.r - this.l + } + + h(){ + return this.b - this.t + } + + toLTWH(){ + const {l, t, r, b} = this + return {l, t, w: r - l, h: b - t } + } + + toLTRB(){ + const {l, t, r, b} = this + return {l, t, r, b} + } + + toLTRBarr(){ + const {l, t, r, b} = this + return [l, t, r, b] + } + + append(x, y){ + this.l = Math.min(this.l, x) + this.t = Math.min(this.t, y) + this.r = Math.max(this.r, x) + this.b = Math.max(this.b, y) + return this + } + + append_many(arr_xy){ + arr_xy.forEach(e => this.append(e.x, e.y)) + return this + } + + scale(k){ + this.r = this.l + (this.r - this.l) * k + this.b = this.t + (this.b - this.t) * k + return this + } + + move(dx, dy){ + this.l += dx + this.t += dy + this.r += dx + this.b += dy + return this + } + + moveto(x, y){ + let dx = x - this.l + let dy = y - this.t + return this.move(dx, dy) + } + + clone(){ + return BBox.fromLTRB(this.l, this.t, this.r, this.b) + } + + // static from_data(data){ + // return data.reduce( + // (s, c) => ({ + // l: Math.min(s.l, c.x), + // t: Math.min(s.t, c.y), + // r: Math.max(s.r, c.x), + // b: Math.max(s.b, c.y), + // }), + // { l: data[0].x, t: data[0].y, r: data[0].x, b: data[0].y } + // ); + // // return data.reduce( + // // (s, c) => ({ + // // l: Math.min(s.l, c.x), + // // t: Math.min(s.t, c.y), + // // r: Math.max(s.r, c.x), + // // b: Math.max(s.b, c.y), + // // }), + // // { l: data[0].x, t: data[0].y, r: data[0].x, b: data[0].y } + // // ); + // } +} \ No newline at end of file diff --git a/src/libs/math.js b/src/libs/math.js index 0ae8e10..bd7e725 100644 --- a/src/libs/math.js +++ b/src/libs/math.js @@ -1,4 +1,10 @@ export default { + /** + * Создает данные дл круговой диаграммы из набора значений v0,v1,v2,v3 -> [{a0: 0, a1: v0}, {a0: v0, a1: v0+v1}, {a0: v0+v1, a1: v0+v1+v2},] + * @param { [Number] } values Значения долей + * @param { boolean } norm Нормировать ли значения диапазонов в отрезок 0-1 + * @returns Массив с диапазонами долей 1,2,3,4 -> [{0,0.1},{0.1,0.3},{0.3,0.6},{0.6,1}] + */ make_ranges(values, norm = true) { let ranges = values.reduce((s, c, i) => [...s, {...c, a0: i && s[i - 1].a1, a1: c + (i && s[i - 1].a1)}], []) let max = ranges[ranges.length - 1].a1 diff --git a/src/moc/styles.json b/src/moc/styles.json index df127ad..45adfca 100644 --- a/src/moc/styles.json +++ b/src/moc/styles.json @@ -1,21 +1,25 @@ { "wellhead": { "font-family": "Times New Roman", - "font-weight": "bold", - "stroke-width": "2.5mm", - "font-size": 500, + "font-weight": "bold", + "font-size": "10pt", + "stroke-width": "0.1pt", "stroke": "#fff", - "fill": "#000" + "fill": "#000", + "dx":1.7, + "dy":"0" }, + "wlf": { "font-family": "Times New Roman", "font-weight": "bold", "stroke-width": "0", - "alignment-baseline": "hanging", - "font-size": 300, + "font-size": "8pt", "font-style": "italic", "stroke": "#fff", - "fill": "#00f" + "fill": "#00f", + "dx": 1.7, + "dy": 3 }, "gpt": { @@ -26,11 +30,16 @@ "opt": { "stroke-width": 10, "stroke": "#000", - "fill": "#B86B41" + "fill": "url(#rg_opt)" }, "wpt": { "stroke-width": 10, "stroke": "#000", - "fill": "#67c2e5" - } + "fill": "url(#rg_wpt)" + }, + "wit": { + "stroke-width": 10, + "stroke": "#000", + "fill": "url(#rg1)" + } } \ No newline at end of file diff --git a/src/svgmap/SvgMap.js b/src/svgmap/SvgMap.js new file mode 100644 index 0000000..2401e04 --- /dev/null +++ b/src/svgmap/SvgMap.js @@ -0,0 +1,140 @@ +import { parse } from "svg-parser"; + +import SvgNode from './SvgNode.js' +import SvgNodes from './SvgNodes.js' + +export default class { + async parse(xml) { + let result1 = parse(xml); + + this.svg = result1.children.filter((x) => x.tagName == "svg")[0]; + } + + /** + * Получить слои документа. Имя слоя можно получить через поле id. + * @returns + */ + get_layers() { + return this.svg.children.filter((x) => x.tagName == "g"); + } + + /** + * Получить данные масштаба карты. + * @returns {w, h, k, u} Ширина в мм, высота в мм, k * v[px] = v[мм], юниты (mm, cm, inch) + */ + get_map_scale() { + // console.log(this.svg) + let vb = this.svg.properties.viewBox.split(" ").map((x) => parseFloat(x)); + let w = this.svg.properties.width; + let h = this.svg.properties.height; + + let units = { + in: 25.4, + mm: 1, + cm: 10, + px: 0.0846646, + // '%': 1 + }; + + const u = Object.keys(units).filter((x) => w.endsWith(x))[0]; + if (!u) throw "Unsupported units"; + + w = Math.round(parseFloat(w.substring(0, w.length - u.length)) * units[u] * 100) / 100; + h = Math.round(parseFloat(h.substring(0, h.length - u.length)) * units[u] * 100) / 100; + let k = vb[2] / w; + + return { w, h, k, u }; + } + + /** + * Достать из слоя опордные скважины + * @param {*} layer Слой в котором 2 последние группы содержат опорные скважины. + * @returns { w1, w2 } Опрорные скважины + */ + extract_anchor(layer) { + let w1 = this.extract_well(layer.children[layer.children.length - 1]); + let w2 = this.extract_well(layer.children[layer.children.length - 2]); + + return { w1, w2 }; + } + + _flat(node) { + if (!node.children) return [node]; + return node.children.reduce((s, c) => s.concat(this._flat(c)), [node]); + } + + /** + * Получить из группы данные скважины (имя, координаты) + * @param {*} pivot_group Svg группа + * @returns { {x, y, name} } + */ + extract_well(pivot_group) { + let flat = this._flat(pivot_group); + let ellipse = flat.filter((x) => x.tagName == "circle" || x.tagName == "ellipse")[0]; + let tr = ellipse.properties.transform + .replace("matrix(", "") + .replace(")", "") + .split(" ") + .map((x) => parseFloat(x)); + + let text = flat.filter((x) => x.type == "text")[0]; + let name = text.value; + return { x: tr[4], y: tr[5], name }; + } + + build_tp_layer(wells, settings){ + const sc = this.get_map_scale() + + function t2r(tons){ + // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + return Math.sqrt(tons / settings.tons_in_cm2 * 100 / Math.PI) + } + + let {styles} = settings + + let defs = new SvgNode("defs") + defs.append(SvgNodes.defs.simple.radialGradient("rg_opt", "#fff", "#B86B41")) + defs.append(SvgNodes.defs.simple.radialGradient("rg_wpt", "#fff", "#67c2e5")) + + let svg = SvgNodes.group(['']).set_attr("id", "WWPT") + // Круги + svg.append(wells.map((x) => SvgNodes.ring_sectors(1.5 * sc.k, t2r(x.wlpt) * sc.k, [{v: x.wopt, style: styles.opt}, {v: x.wwpt, style: styles.wpt}]).move(x.lx, x.ly))); + // Знак скважины + svg.append(wells.map((x) => SvgNodes.circle(1.5 * sc.k).move(x.lx, x.ly).set_style(styles.wellhead))); + // Имя скважины + svg.append(wells.map((x) => SvgNodes.text(x.well).move(x.lx + styles.wellhead.dx * sc.k, x.ly + styles.wellhead.dy * sc.k).set_style(styles.wellhead))); + // Обводненность + svg.append(wells.map((x) => SvgNodes.text(`${Math.round(x.wlf * 1000)/10 || ''}%`).move(x.lx + styles.wlf.dx * sc.k, x.ly + styles.wlf.dy * sc.k).set_style(styles.wlf))); + + return {defs, svg} + } + + build_ti_layer(wells, settings){ + const sc = this.get_map_scale() + + function t2r(tons){ + // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + return Math.sqrt(tons / settings.tons_in_cm2 * 100 / Math.PI) + } + + let {styles} = settings + + let defs = SvgNodes.container() + defs.append(SvgNodes.defs.simple.radialGradient("rg1", "#fff", "#c1ff5e")) + + let svg = SvgNodes.group(['']).set_attr("id", "WWIT") + // Круги + // let g_rings = SvgNodes.group(['']).set_attr("id", "Круги") + // svg.append(g_rings) + svg.append(wells.map((x) => SvgNodes.circle(t2r(x.wwit) * sc.k).set_style(styles.wit).move(x.lx, x.ly))); + // Знак скважины + svg.append(wells.map((x) => SvgNodes.circle(1.5 * sc.k).move(x.lx, x.ly).set_style(styles.wellhead))); + // Имя скважины + svg.append(wells.map((x) => SvgNodes.text(x.well).move(x.lx + styles.wellhead.dx * sc.k, x.ly + styles.wellhead.dy * sc.k).set_style(styles.wellhead))); + // Обводненность + svg.append(wells.map((x) => SvgNodes.text(`${Math.round(x.wlf * 1000)/10 || ''}%`).move(x.lx + styles.wlf.dx * sc.k, x.ly + styles.wlf.dy * sc.k).set_style(styles.wlf))); + + return {defs, svg} + } + +} diff --git a/src/svgmap/SvgMapBuilder.js b/src/svgmap/SvgMapBuilder.js index 9928544..4cdec17 100644 --- a/src/svgmap/SvgMapBuilder.js +++ b/src/svgmap/SvgMapBuilder.js @@ -1,24 +1,144 @@ -export default class SvgMapBulder{ - let WellsRenderer = { - // [{x,y}] - heads(wells, r, style) { - return wells.map((w) => SvgNodes.circle(r).set_style(style).move(w.x, w.y)); - }, +import SvgNode from "./SvgNode.js"; +import SvgNodes from "./SvgNodes.js"; - // [{x,y,name}] - names(wells, style, shift) { - return wells.map((w) => - SvgNodes.text(w.name) - .set_style(style) - .move(w.x + shift.x, w.y + shift.y) - ); - }, +export default class { + /** + * Преобразовать размер шрифта в единицы карты + * @param { string | Number } v Значение для перевода "10mm", "22pt" + * @param {*} ppu Pixel per unit + * @returns + */ + fontsize2ppu(v, ppu) { + if (typeof v !== "string") return v; - // {x,y,ring[1,2,3,4,5]} - ring(x, y, r0, r1, a0, a1, style) { - return SvgNodes.ring_sector(r0, r1, a0, a1).move(x, y).set_style(style); - }, - }; + if (v.endsWith("mm")) { + return parseFloat(v.substring(0, v.length - 2) * ppu * 100) / 69; + } + if (v.endsWith("pt")) { + return (parseFloat(v.slice(0, v.length - 2)) * ppu * 100) / 283.46; + } + } + /** + * Перегоняет стиль в единицы карты (возвращает новый объект) + * @param {*} style Объект стиля для преобразования + * @param {*} ppu Pixel per unit + */ + update_style(style, ppu) { + let s = Object.assign({}, style); + const convertable = ["font-size", "stroke-width"]; + Object.keys(s).forEach((k) => { + if (convertable.includes(k)) s[k] = this.fontsize2ppu(s[k], ppu); + }); + return s; + } -} \ No newline at end of file + /** + * Перегоняет все стили в единицы карты (возвращает новый объект) + * @param {*} style Объект стиля для преобразования + * @param {*} ppu Pixel per unit + */ + update_styles(styles, ppu) { + return Object.keys(styles).reduce((s, c) => ({ ...s, [c]: this.update_style(styles[c], ppu) }), {}); + } + + build_tp_layer(wells, settings) { + function t2r(tons) { + // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + return Math.sqrt(((tons / settings.tons_in_cm2) * 100) / Math.PI); + } + + let { ppu, styles } = settings; + + let defs = new SvgNode("defs"); + defs.append(SvgNodes.defs.simple.radialGradient("rg_opt", "#fff", "#B86B41")); + defs.append(SvgNodes.defs.simple.radialGradient("rg_wpt", "#fff", "#67c2e5")); + + let svg = SvgNodes.group(['']).set_attr("id", "WWPT"); + // Круги + svg.append( + wells.map((x) => + SvgNodes.ring_sectors(1.5 * ppu, t2r(x.wlpt) * ppu, [ + { v: x.wopt, style: styles.opt }, + { v: x.wwpt, style: styles.wpt }, + ]).move(x.lx, x.ly) + ) + ); + // Знак скважины + svg.append( + wells.map((x) => + SvgNodes.circle(1.5 * ppu) + .move(x.lx, x.ly) + .set_style(styles.wellhead) + ) + ); + // Имя скважины + svg.append( + wells.map((x) => + SvgNodes.text(x.well) + .move(x.lx + styles.wellhead.dx * ppu, x.ly + styles.wellhead.dy * ppu) + .set_style(styles.wellhead) + ) + ); + // Обводненность + svg.append( + wells.map((x) => + SvgNodes.text(`${Math.round(x.wlf * 1000) / 10 || ""}%`) + .move(x.lx + styles.wlf.dx * ppu, x.ly + styles.wlf.dy * ppu) + .set_style(styles.wlf) + ) + ); + + return { defs, svg }; + } + + build_ti_layer(wells, settings) { + function t2r(tons) { + // tonns/mm2 = tonns/cm2 / 100. S(mm)=Pi*r*r=tons/tons_in_cm2*100. r = sqrt(tons/tons_in_cm2*100 / PI) + return Math.sqrt(((tons / settings.tons_in_cm2) * 100) / Math.PI); + } + + let { ppu, styles } = settings; + + let defs = new SvgNode("defs"); + defs.append(SvgNodes.defs.simple.radialGradient("rg1", "#fff", "#c1ff5e")); + + let svg = SvgNodes.group(['']).set_attr("id", "WWIT"); + // Круги + // let g_rings = SvgNodes.group(['']).set_attr("id", "Круги") + // svg.append(g_rings) + svg.append( + wells.map((x) => + SvgNodes.circle(t2r(x.wwit) * ppu) + .set_style(styles.wit) + .move(x.lx, x.ly) + ) + ); + // Знак скважины + svg.append( + wells.map((x) => + SvgNodes.circle(1.5 * ppu) + .move(x.lx, x.ly) + .set_style(styles.wellhead) + ) + ); + // Имя скважины + svg.append( + wells.map((x) => + SvgNodes.text(x.well) + .move(x.lx + styles.wellhead.dx * ppu, x.ly + styles.wellhead.dy * ppu) + .set_style(styles.wellhead) + ) + ); + // Обводненность + svg.append( + wells.map((x) => + SvgNodes.text(`${Math.round(x.wlf * 1000) / 10 || ""}%`) + .move(x.lx + styles.wlf.dx * ppu, x.ly + styles.wlf.dy * ppu) + .set_style(styles.wlf) + ) + ); + + return { defs, svg }; + } +} diff --git a/src/svgmap/SvgMapParser.js b/src/svgmap/SvgMapParser.js deleted file mode 100644 index 006fe31..0000000 --- a/src/svgmap/SvgMapParser.js +++ /dev/null @@ -1,74 +0,0 @@ -// import convert from 'xml-js/index.js' -// import { XMLParser, XMLBuilder, XMLValidator} from 'fast-xml-parser' -import { parse } from 'svg-parser' - -// var convert = require('xml-js'); - -export default class { - async parse(xml){ - let result1 = parse(xml); - - this.svg = result1.children.filter(x => x.tagName =='svg')[0] - } - - get_layers(){ - return this.svg.children.filter(x => x.tagName == 'g') - } - - get_map_scale(){ - // console.log(this.svg) - let vb = this.svg.properties.viewBox.split(' ').map(x => parseFloat(x)) - let w = this.svg.properties.width - let h = this.svg.properties.height - - let units = { - 'in': 25.4, - 'mm': 1, - 'cm': 10, - 'px': 0.0846646, - // '%': 1 - } - - const u = Object.keys(units).filter(x => w.endsWith(x))[0] - if (!u) throw "Unsupported units" - - w = Math.round(parseFloat(w.substring(0, w.length - u.length)) * units[u] * 100) / 100 - h = Math.round(parseFloat(h.substring(0, h.length - u.length)) * units[u] * 100) / 100 - let k = vb[2] / w - - return {w, h, k, u} - } - - extract_anchor(layer){ - let w1 = this.extract_well(layer.children[layer.children.length - 1]) - let w2 = this.extract_well(layer.children[layer.children.length - 2]) - - return{w1, w2} - } - - _flat(node){ - if (!node.children) return [node]; - return node.children.reduce((s, c) => s.concat(this._flat(c)), [node]) - } - - extract_well(pivot_group){ - let scale = this.get_map_scale() - - let flat = this._flat(pivot_group) - ///// console.log('pivot_group', pivot_group.children) - // console.log('FLAT---', flat) - let ellipse = flat.filter(x => x.tagName =='circle' || x.tagName =='ellipse')[0] - // console.log('ellipse', ellipse) - let tr = ellipse.properties.transform.replace('matrix(', '').replace(')', '').split(' ').map(x => parseFloat(x)) - // console.log('tr', tr) - // let x = ellipse.properties.transform - // let y = ellipse.cy.baseVal.value - - let text = flat.filter(x => x.type =='text')[0] - // console.log('text', text) - let name = text.value - return {x: tr[4], y: tr[5], name} - } - - -} \ No newline at end of file diff --git a/src/svgmap/SvgNode.js b/src/svgmap/SvgNode.js index de9ef46..be12bbf 100644 --- a/src/svgmap/SvgNode.js +++ b/src/svgmap/SvgNode.js @@ -5,8 +5,8 @@ export default class SvgNode { this.items = items || [] } - append(items){ - this.items.push(items) + append(...items){ + this.items.push(...items) return this } @@ -49,13 +49,16 @@ export default class SvgNode { } render() { - let attrs = Object.keys(this.attrs).map(x => ` ${x}="${this.attrs[x]}"`).join('') + let attrs = Object.keys(this.attrs).filter(x => this.attrs[x]).map(x => ` ${x}="${this.attrs[x]}"`).join('') // console.log(this.style ? Object.keys(this.style).map(x => `${x}:${this.style[x]}`) : '') let style = this.style ? ' style="' + Object.keys(this.style).map(x => `${x}:${this.style[x]}`).join('; ') + '"' : '' let transfrom = this.transform ? ` transform="translate(${this.transform.x},${this.transform.y})"` : '' let items = this.items ? this.items.map(x => this._render_node(x)).join('') : '' // console.log('this.items', this.items, items) - return `<${this.tag}${attrs}${style}${transfrom}>${items}` + if (this.tag) + return `<${this.tag}${attrs}${style}${transfrom}>${items}` + else + return items } } \ No newline at end of file diff --git a/src/svgmap/SvgNodes.js b/src/svgmap/SvgNodes.js index ded6616..b692b74 100644 --- a/src/svgmap/SvgNodes.js +++ b/src/svgmap/SvgNodes.js @@ -26,6 +26,10 @@ export default { }) }, + container(items){ + return new SvgNode(null, null, items) + }, + group(items){ return new SvgNode("g", null, items) }, @@ -86,6 +90,26 @@ export default { node.items = [text]; node.set_attrs({x: 0, y: 0}) return node; + }, + + defs: { + simple: { + radialGradient(id, color0, color1){ + let s0 = new SvgNode("stop", {offset: "0%", "stop-color": color0}) + let s1 = new SvgNode("stop", {offset: "100%", "stop-color": color1}) + return new SvgNode("radialGradient", {id}).append(s0, s1) + } + }, + + radialGradient(id, cx, cy, r){ + return new SvgNode("radialGradient", {id, cx, cy, r}) + }, + + stop(offset, color){ + return new SvgNode("stop", {offset, "stop-color": color}) + }, + + } };