├── .env ├── .gitignore ├── LICENSE.md ├── README.md ├── client ├── Dockerfile ├── app.py ├── requirements.txt ├── static │ ├── client-alarms.js │ ├── client-datapoints.js │ ├── client-events.js │ ├── client-gis.js │ ├── client-schema.js │ ├── client.js │ ├── css │ │ ├── L.Control.Sidebar.css │ │ ├── L.Control.Sidebar.scss │ │ ├── easy-button.css │ │ ├── fontawesome.css │ │ ├── images │ │ │ ├── layers-2x.png │ │ │ ├── layers.png │ │ │ ├── marker-icon-2x.png │ │ │ ├── marker-icon.png │ │ │ ├── marker-shadow.png │ │ │ ├── spritesheet-2x.png │ │ │ ├── spritesheet.png │ │ │ ├── spritesheet.svg │ │ │ ├── svg-icon-2x.png │ │ │ ├── svg-icon.png │ │ │ └── svg-icon.svg │ │ ├── layout.css │ │ ├── leaflet.css │ │ ├── leaflet.draw-src.css │ │ ├── leaflet.draw.css │ │ ├── leaflet.draw.svg.css │ │ ├── leaflet.modal.css │ │ ├── leaflet.modal.min.css │ │ └── tabulator │ │ │ ├── tabulator.css │ │ │ ├── tabulator.css.map │ │ │ ├── tabulator.min.css │ │ │ ├── tabulator.min.css.map │ │ │ ├── tabulator_bootstrap3.css │ │ │ ├── tabulator_bootstrap3.css.map │ │ │ ├── tabulator_bootstrap3.min.css │ │ │ ├── tabulator_bootstrap3.min.css.map │ │ │ ├── tabulator_bootstrap4.css │ │ │ ├── tabulator_bootstrap4.css.map │ │ │ ├── tabulator_bootstrap4.min.css │ │ │ ├── tabulator_bootstrap4.min.css.map │ │ │ ├── tabulator_bulma.css │ │ │ ├── tabulator_bulma.css.map │ │ │ ├── tabulator_bulma.min.css │ │ │ ├── tabulator_bulma.min.css.map │ │ │ ├── tabulator_materialize.css │ │ │ ├── tabulator_materialize.css.map │ │ │ ├── tabulator_materialize.min.css │ │ │ ├── tabulator_materialize.min.css.map │ │ │ ├── tabulator_midnight.css │ │ │ ├── tabulator_midnight.css.map │ │ │ ├── tabulator_midnight.min.css │ │ │ ├── tabulator_midnight.min.css.map │ │ │ ├── tabulator_modern.css │ │ │ ├── tabulator_modern.css.map │ │ │ ├── tabulator_modern.min.css │ │ │ ├── tabulator_modern.min.css.map │ │ │ ├── tabulator_semanticui.css │ │ │ ├── tabulator_semanticui.css.map │ │ │ ├── tabulator_semanticui.min.css │ │ │ ├── tabulator_semanticui.min.css.map │ │ │ ├── tabulator_simple.css │ │ │ ├── tabulator_simple.css.map │ │ │ ├── tabulator_simple.min.css │ │ │ ├── tabulator_simple.min.css.map │ │ │ ├── tabulator_site.css │ │ │ ├── tabulator_site.css.map │ │ │ ├── tabulator_site.min.css │ │ │ └── tabulator_site.min.css.map │ ├── favicon.svg │ ├── lib │ │ ├── L.Control.Sidebar.js │ │ ├── L.Grid.js │ │ ├── L.Modal.js │ │ ├── L.Modal.js.map │ │ ├── L.Modal.min.js │ │ ├── L.Modal.min.js.map │ │ ├── easy-button.js │ │ ├── jquery-3.5.1.min.js │ │ ├── jquery.min.js │ │ ├── leaflet-hash.js │ │ ├── leaflet-src.js │ │ ├── leaflet-src.js.map │ │ ├── leaflet.draw-src.js │ │ ├── leaflet.draw-src.map │ │ ├── leaflet.draw.js │ │ ├── leaflet.draw.svg.js │ │ ├── leaflet.js │ │ ├── leaflet.js.map │ │ ├── socket.io.js │ │ ├── socket.io.min.js.map │ │ └── tabulator │ │ │ ├── jquery_wrapper.js │ │ │ ├── tabulator.js │ │ │ ├── tabulator.js.map │ │ │ ├── tabulator.min.js │ │ │ ├── tabulator.min.js.map │ │ │ ├── tabulator_esm.js │ │ │ ├── tabulator_esm.js.map │ │ │ ├── tabulator_esm.min.js │ │ │ └── tabulator_esm.min.js.map │ └── webfonts │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 └── templates │ └── index.html ├── diagram_detail.drawio.png ├── diagram_rtu.drawio.png ├── diagram_sim.drawio.png ├── diagram_test.drawio.png ├── docker-compose.yml ├── env.template ├── generate_new_env_secrets.sh ├── grafana ├── dashboard.yaml ├── dashboards │ └── main-dashboard.json └── datasources │ └── automatic.yaml ├── iec61850_fep ├── COPYING ├── Dockerfile ├── README.md ├── app.py ├── lib61850.py ├── libiec61850client.py └── requirements.txt ├── ifs ├── COPYING ├── Dockerfile ├── README.md ├── app.py ├── lib60870.py ├── libiec60870client.py └── requirements.txt ├── mongodb ├── backup_maps │ ├── alarm_logic.json │ ├── alarm_table.json │ ├── dataprovider_list.json │ ├── gis_objects.json │ ├── schema_geojson.json │ ├── schema_objects.json │ └── svg_templates.json ├── generate_key.sh └── mongo-init.js ├── scada.drawio.png ├── screenshot.png ├── solver ├── Dockerfile ├── app.py └── requirements.txt ├── static_dataprovider ├── Dockerfile ├── app.py ├── requirements.txt └── saved_static_datapoints.pkl ├── svg_templates ├── ct.svg ├── gnd.svg ├── msr_active.svg ├── msr_passive.svg ├── swi_round.svg ├── tekst_1line_center.svg ├── tekst_1line_left.svg ├── tekst_3line_center.svg ├── tekst_3line_left.svg ├── tester.html ├── vt.svg └── xcbr.svg ├── test_gateway ├── COPYING ├── Dockerfile ├── README.md ├── app.py ├── config.local.ini ├── config.remote.ini ├── lib60870.py └── libiec60870server.py └── todo.txt /.env: -------------------------------------------------------------------------------- 1 | #mongodb 2 | MONGO_INITDB_ROOT_USERNAME=root 3 | MONGO_INITDB_ROOT_PASSWORD=root_secret 4 | MONGO_USER_DB=scada 5 | MONGO_USER_USERNAME=dbuser 6 | MONGO_USER_PASSWORD="mongo_secret" 7 | MONGO_REPLICA_SET_NAME=rs0 8 | 9 | #redis 10 | REDIS_PASSWORD="redis_secret" 11 | 12 | #influxdb 13 | INFLUXDB_DATA_ENGINE=tsm1 14 | INFLUXDB_REPORTING_DISABLED=true 15 | DOCKER_INFLUXDB_INIT_MODE=setup 16 | DOCKER_INFLUXDB_INIT_USERNAME=admin 17 | DOCKER_INFLUXDB_INIT_PASSWORD=administrator 18 | DOCKER_INFLUXDB_INIT_ORG=scada 19 | DOCKER_INFLUXDB_INIT_BUCKET=bucket_1 20 | DOCKER_INFLUXDB_INIT_ADMIN_TOKEN="influxdb_secret" 21 | 22 | # grafana 23 | GF_INSTALL_PLUGINS= 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mongodb/mongodb.key 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | COPY ./requirements.txt /srv/client/requirements.txt 4 | 5 | RUN cd /srv/client && pip3 install --no-cache-dir -r requirements.txt 6 | 7 | COPY . /srv/client 8 | 9 | WORKDIR /srv/client 10 | 11 | EXPOSE 5000 12 | 13 | CMD ["python3","app.py","remote"] 14 | -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | dnspython==2.2.1 2 | flask-socketio==5.3.2 3 | eventlet==0.33.3 4 | redis 5 | influxdb-client 6 | pymongo 7 | -------------------------------------------------------------------------------- /client/static/client-alarms.js: -------------------------------------------------------------------------------- 1 | function init_alarm(){ 2 | //define data array 3 | socket.on('update_alarm_table', function (json) { 4 | console.log("UPDATE TRIGGER"); 5 | table.replaceData(json).then(function(){ 6 | table.getRows().forEach((row_element) => { 7 | let alarm = row_element.getData().alarm; 8 | let ack = row_element.getData().acknowledged; 9 | let open = row_element.getData().open; 10 | 11 | let severity = row_element.getData().severity; 12 | row_element.getElement().style.animation = ""; 13 | if(alarm == true && ack == false && open == true){ 14 | if(severity == 0) { row_element.getElement().style.animation = "blinker_info 1s linear infinite"; } 15 | else if (severity == 1) {row_element.getElement().style.animation = "blinker_low 1s linear infinite"; } 16 | else if (severity == 2) {row_element.getElement().style.animation = "blinker_medium 1s linear infinite"; } 17 | else if (severity == 3) {row_element.getElement().style.animation = "blinker_high 1s linear infinite"; } 18 | else if (severity == 4) {row_element.getElement().style.animation = "blinker_critical 1s linear infinite"; } 19 | else{ row_element.getElement().style.animation = "blinker_high 1s linear infinite"; } 20 | } else if(alarm == false && ack == false && open == true){ 21 | row_element.getElement().style.backgroundColor = "orange"; 22 | row_element.getElement().style.color = "black"; 23 | } else if(alarm == true && ack == true && open == true){ 24 | row_element.getElement().style.backgroundColor = "yellow"; 25 | row_element.getElement().style.color = "black"; 26 | } else if(alarm == false && ack == true && open == true){ 27 | row_element.getElement().style.backgroundColor = "green"; 28 | row_element.getElement().style.color = "black"; 29 | }else { 30 | row_element.getElement().style.backgroundColor = "black"; 31 | row_element.getElement().style.color = "white"; 32 | } 33 | }); 34 | }); //load data array 35 | }); 36 | 37 | 38 | var table = new Tabulator("#mmi_svg", { 39 | //data:tabledata, //load row data from array 40 | layout:"fitColumns", //fit columns to width of table 41 | responsiveLayout:"hide", //hide columns that dont fit on the table 42 | //tooltips:true, //show tool tips on cells 43 | addRowPos:"top", //when adding a new row, add it to the top of the table 44 | history:true, //allow undo and redo actions on the table 45 | pagination:"local", //paginate the data 46 | paginationSize:7, //allow 7 rows per page of data 47 | paginationCounter:"rows", //display count of paginated rows in footer 48 | movableColumns:true, //allow column order to be changed 49 | resizableRows:true, //allow row order to be changed 50 | initialSort:[ //set the initial sort order of the data 51 | {column:"time", dir:"asc"}, 52 | ], 53 | columns:[ //define the table columns 54 | {title:"Time", field:"time",hozAlign:"center", formatter:"plaintext"}, 55 | {title:"Element", field:"element", hozAlign:"left", formatter:"plaintext"}, 56 | {title:"Message", field:"message", hozAlign:"left", formatter:"textarea"}, 57 | {title:"Alarm", field:"alarm", width:90, hozAlign:"center", formatter:"tickCross", sorter:"boolean", editor:"tickCross" }, 58 | {title:"Ack", field:"acknowledged", width:90, hozAlign:"center", formatter:"tickCross", sorter:"boolean", editor:"tickCross" }, 59 | {title:"Open", field:"open", width:90, hozAlign:"center", formatter:"tickCross", sorter:"boolean", editor:"tickCross" }, 60 | ], 61 | }); 62 | 63 | //only show open alarms 64 | //table.setFilter("open", "=", "true"); 65 | table.on("cellEdited", function(cell){ 66 | let data = cell.getData(); 67 | document.querySelector("#alarm_comment").value = ""; 68 | //acknowledge alarm/close alarm 69 | socket.emit('update_alarm_state', data ); 70 | 71 | table.refreshFilter(); 72 | }); 73 | 74 | table.on("cellDblClick", function(e, cell){ 75 | let field = cell.getField(); 76 | if(field === "alarm" || field === "acknowledged" || field === "open"){ 77 | let inv = !cell.getValue(); 78 | cell.setValue(inv); 79 | } 80 | else{ 81 | hideAllModalWindows(); 82 | let row = cell.getRow(); 83 | //alert("Cell clicked: " + cell.getField() + " : " + row.getData().details); //test, should become modal html dialog 84 | let modalAlarmTarget = document.querySelector("#modal-alarm"); 85 | document.querySelector(".modal-fader").className += " active"; 86 | modalAlarmTarget.className += " active"; 87 | document.querySelector("#modal-alarm-time").innerHTML = "Time:" + row.getData().time; 88 | document.querySelector("#modal-alarm-severity").innerHTML = "Severity:" + row.getData().severity; 89 | document.querySelector("#modal-alarm-element").innerHTML = "Element:" + row.getData().element; 90 | document.querySelector("#modal-alarm-datapoint").innerHTML = "Datapoint:" + row.getData().datapoint; 91 | document.querySelector("#modal-alarm-value").innerHTML = "Value:" + row.getData().value; 92 | document.querySelector("#modal-alarm-message").innerHTML = "Message:" + row.getData().message; 93 | document.querySelector("#modal-alarm-details").innerHTML = row.getData().details; 94 | document.querySelector("#alarm_comment").value = ""; 95 | var currentRow = row.getData() 96 | document.querySelector("#modal-silence-alarm").onclick = function () { 97 | currentRow.alarm = false; 98 | currentRow.comment = document.querySelector("#alarm_comment").value; 99 | socket.emit('update_alarm_state', currentRow ); 100 | table.refreshFilter(); 101 | hideAllModalWindows(); 102 | }; 103 | document.querySelector("#modal-ack-alarm").onclick = function () { 104 | currentRow.acknowledged = true; 105 | currentRow.comment = document.querySelector("#alarm_comment").value; 106 | socket.emit('update_alarm_state', currentRow ); 107 | table.refreshFilter(); 108 | hideAllModalWindows(); 109 | }; 110 | 111 | document.querySelector("#modal-close-alarm").onclick = function () { 112 | currentRow.open = false; 113 | currentRow.comment = document.querySelector("#alarm_comment").value; 114 | socket.emit('update_alarm_state', currentRow ); 115 | table.refreshFilter(); 116 | hideAllModalWindows(); 117 | }; 118 | } 119 | 120 | }); 121 | 122 | document.querySelector(".modal-fader").addEventListener("click", function () { 123 | hideAllModalWindows(); 124 | }); 125 | 126 | init_alarm_dialog(); 127 | init_editRules(); 128 | 129 | refresh_alarm_table(); 130 | } 131 | 132 | function refresh_alarm_table(){ 133 | socket.emit('get_alarm_table', {} ); 134 | } 135 | 136 | function init_alarm_dialog() { 137 | document.querySelectorAll(".aPop-close").forEach(function (closeBtn) { 138 | closeBtn.addEventListener("click", function () { 139 | document.querySelector("#alarm_comment").value = ""; 140 | hideAllModalWindows(); 141 | }); 142 | }); 143 | } 144 | 145 | 146 | function init_editRules() { 147 | 148 | document.querySelectorAll(".open-modal").forEach(function (trigger) { 149 | trigger.addEventListener("click", function () { 150 | hideAllModalWindows(); 151 | showModalWindow(this); 152 | }); 153 | }); 154 | 155 | document.querySelectorAll(".modal-save").forEach(function (saveBtn) { 156 | saveBtn.addEventListener("click", function () { 157 | var modalTarget = document.querySelector("#modal-1"); 158 | let error_msg = document.querySelector('#modal-1_error-message'); 159 | try{ 160 | let rules = modalTarget.childNodes[3].value; 161 | let temp = JSON.parse(rules); 162 | socket.emit('save_alarm_rules', rules, 163 | function(ret){ 164 | if(ret == true){ 165 | console.log("save ok"); 166 | error_msg.innerHTML = "Saved"; 167 | error_msg.style = "color:green"; 168 | }else{ 169 | alert("could not save alarm rules"); 170 | } 171 | } 172 | ); 173 | } catch(err) { 174 | error_msg.innerHTML = err.message; 175 | error_msg.style = "color:red"; 176 | } 177 | }); 178 | }); 179 | 180 | document.querySelectorAll(".modal-hide").forEach(function (closeBtn) { 181 | closeBtn.addEventListener("click", function () { 182 | hideAllModalWindows(); 183 | }); 184 | }); 185 | 186 | var btn = document.createElement("button"); 187 | var t = document.createTextNode("edit rules"); 188 | btn.setAttribute("class","mapButton"); 189 | btn.appendChild(t); 190 | document.querySelector("#mmi_svg").parentElement.insertBefore(btn,document.querySelector("#mmi_svg"));//,null); 191 | btn.addEventListener("click", function () { 192 | hideAllModalWindows(); 193 | show_edit_alarm_ModalWindow(this); 194 | }); 195 | 196 | } 197 | 198 | function show_edit_alarm_ModalWindow (buttonEl) { 199 | var modalTarget = document.querySelector("#modal-1"); 200 | document.querySelector(".modal-fader").className += " active"; 201 | modalTarget.className += " active"; 202 | 203 | document.querySelector('#modal-1_error-message').innerHTML = ""; 204 | document.querySelector('#modal-1_error-message').style = "color:black"; 205 | 206 | socket.emit('get_alarm_rules', { }, 207 | function(ret){ 208 | modalTarget.childNodes[3].value = ret; 209 | } 210 | ); 211 | } 212 | 213 | -------------------------------------------------------------------------------- /client/static/client-datapoints.js: -------------------------------------------------------------------------------- 1 | function init_dataproviders(){ 2 | 3 | socket.on('update_dataprovider_status', function (json) { 4 | table.getRows().forEach((row_element) => { 5 | if(row_element.getData().dataprovider == json.dataprovider){ 6 | row_element.getData().online = json.online; 7 | } 8 | }); 9 | }); 10 | 11 | function refresh_dataproviders_table(){ 12 | socket.emit('get_dataproviders',{},function(result){ 13 | //console.log(result); 14 | table.replaceData(result); 15 | }); 16 | } 17 | 18 | var table = new Tabulator("#mmi_svg", { 19 | autoColumns:true, //create columns from data field names 20 | //data:tabledata, //load row data from array 21 | layout:"fitColumns", //fit columns to width of table 22 | responsiveLayout:"hide", //hide columns that dont fit on the table 23 | //tooltips:true, //show tool tips on cells 24 | addRowPos:"top", //when adding a new row, add it to the top of the table 25 | pagination:"local", //paginate the data 26 | //paginationSize:7, //allow 7 rows per page of data 27 | paginationCounter:"rows", //display count of paginated rows in footer 28 | movableColumns:true, //allow column order to be changed 29 | resizableRows:true, //allow row order to be changed 30 | initialSort:[ //set the initial sort order of the data 31 | {column:"IFS", dir:"asc"}, 32 | ], 33 | columns:[ //define the table columns 34 | {title:"IFS", field:"IFS", hozAlign:"left", formatter:"plaintext"}, 35 | {title:"type", field:"type", hozAlign:"left", formatter:"plaintext"}, 36 | {title:"dataprovider", field:"dataprovider", hozAlign:"left", formatter:"plaintext"}, 37 | {title:"enabled", field:"enabled", width:90, hozAlign:"center", formatter:"tickCross", sorter:"boolean"}, 38 | {title:"online", field:"online", width:90, hozAlign:"center", formatter:"tickCross", sorter:"boolean"}, 39 | ], 40 | }); 41 | 42 | table.on("rowDblClick", function(e, row){ 43 | //alert("Cell clicked: " + row.getData() + " "); //test 44 | var modalTarget = document.querySelector("#modal-2"); 45 | document.querySelector(".modal-fader").className += " active"; 46 | modalTarget.className += " active"; 47 | 48 | modalTarget.dataprovider_id = row.getData().id; 49 | document.querySelectorAll(".modal-delete").forEach(function (deleteBtn) { 50 | deleteBtn.style.display = "inline-block"; 51 | }); 52 | modalTarget.childNodes[3].value = '{\n \ 53 | "dataprovider":"' + row.getData().dataprovider + '",\n \ 54 | "enabled": '+ row.getData().enabled +',\n \ 55 | "IFS": "'+ row.getData().IFS +'",\n \ 56 | "type": "'+ row.getData().type +'",\n \ 57 | "config": "'+ JSON.stringify(row.getData().config, null, 2) +'"\n\ 58 | }'; 59 | }); 60 | 61 | init_add_datapoints(); 62 | refresh_dataproviders_table(); 63 | } 64 | 65 | function init_add_datapoints() { 66 | 67 | document.querySelectorAll(".open-modal").forEach(function (trigger) { 68 | trigger.addEventListener("click", function () { 69 | hideAllModalWindows(); 70 | showModalWindow(this); 71 | }); 72 | }); 73 | 74 | document.querySelectorAll(".modal-save").forEach(function (saveBtn) { 75 | saveBtn.addEventListener("click", function () { 76 | var modalTarget = document.querySelector("#modal-2"); 77 | let error_msg = document.querySelector('#modal-2_error-message'); 78 | try{ 79 | let dataprovider = modalTarget.childNodes[3].value; 80 | let temp = JSON.parse(dataprovider); 81 | socket.emit('edit_dataprovider', dataprovider, 82 | function(ret){ 83 | location.reload(); 84 | if(ret == true){ 85 | console.log("save ok"); 86 | error_msg.innerHTML = "Saved"; 87 | error_msg.style = "color:green"; 88 | }else{ 89 | alert("could not save dataprovider"); 90 | } 91 | } 92 | ); 93 | } catch(err) { 94 | error_msg.innerHTML = err.message; 95 | error_msg.style = "color:red"; 96 | } 97 | }); 98 | }); 99 | 100 | document.querySelectorAll(".modal-delete").forEach(function (deleteBtn) { 101 | deleteBtn.addEventListener("click", function () { 102 | var modalTarget = document.querySelector("#modal-2"); 103 | let error_msg = document.querySelector('#modal-2_error-message'); 104 | try{ 105 | if('dataprovider_id' in modalTarget && modalTarget.dataprovider_id[0] === '_'){ 106 | socket.emit('delete_dataprovider', modalTarget.dataprovider_id, 107 | function(ret){ 108 | location.reload(); 109 | if(ret == true){ 110 | console.log("save ok"); 111 | error_msg.innerHTML = "Saved"; 112 | error_msg.style = "color:green"; 113 | }else{ 114 | alert("could not delete dataprovider"); 115 | } 116 | } 117 | ); 118 | } 119 | } catch(err) { 120 | error_msg.innerHTML = err.message; 121 | error_msg.style = "color:red"; 122 | } 123 | }); 124 | }); 125 | 126 | document.querySelectorAll(".modal-hide").forEach(function (closeBtn) { 127 | closeBtn.addEventListener("click", function () { 128 | hideAllModalWindows(); 129 | }); 130 | }); 131 | 132 | document.querySelector(".modal-fader").addEventListener("click", function () { 133 | hideAllModalWindows(); 134 | }); 135 | 136 | var btn = document.createElement("button"); 137 | var t = document.createTextNode("add dataprovider"); 138 | btn.setAttribute("class","mapButton"); 139 | btn.appendChild(t); 140 | document.querySelector("#mmi_svg").parentElement.insertBefore(btn,document.querySelector("#mmi_svg"));//,null); 141 | btn.addEventListener("click", function () { 142 | hideAllModalWindows(); 143 | show_add_dataprovider_ModalWindow(this); 144 | }); 145 | 146 | } 147 | 148 | function show_add_dataprovider_ModalWindow (buttonEl) { 149 | var modalTarget = document.querySelector("#modal-2"); 150 | document.querySelector(".modal-fader").className += " active"; 151 | modalTarget.className += " active"; 152 | 153 | document.querySelector('#modal-2_error-message').innerHTML = ""; 154 | document.querySelector('#modal-2_error-message').style = "color:black"; 155 | 156 | document.querySelectorAll(".modal-delete").forEach(function (deleteBtn) { 157 | deleteBtn.style.display = "none"; 158 | }); 159 | modalTarget.childNodes[3].value = '{ \n \ 160 | "dataprovider":"[IP]:[port]",\n \ 161 | "enabled": [false/true],\n \ 162 | "IFS": "[IFS name]",\n \ 163 | "type": "[Protocol type]",\n \ 164 | "config": {}\n \ 165 | }'; 166 | } 167 | 168 | 169 | -------------------------------------------------------------------------------- /client/static/client-events.js: -------------------------------------------------------------------------------- 1 | function init_events(){ 2 | 3 | socket.on('update_event_table', function (json) { 4 | //write events to table 5 | table.replaceData(json); 6 | }); 7 | 8 | socket.on('add_event_to_table', function (json) { 9 | //add a event to table 10 | table.addData(json) 11 | }); 12 | 13 | var table = new Tabulator("#mmi_svg", { 14 | autoColumns:true, //create columns from data field names 15 | //data:tabledata, //load row data from array 16 | layout:"fitColumns", //fit columns to width of table 17 | responsiveLayout:"hide", //hide columns that dont fit on the table 18 | //tooltips:true, //show tool tips on cells 19 | addRowPos:"top", //when adding a new row, add it to the top of the table 20 | pagination:"local", //paginate the data 21 | //paginationSize:7, //allow 7 rows per page of data 22 | paginationCounter:"rows", //display count of paginated rows in footer 23 | movableColumns:true, //allow column order to be changed 24 | resizableRows:true, //allow row order to be changed 25 | initialSort:[ //set the initial sort order of the data 26 | {column:"time", dir:"asc"}, 27 | ], 28 | columns:[ //define the table columns 29 | {title:"Time", field:"time",hozAlign:"center", formatter:"plaintext"}, 30 | {title:"Element", field:"element", hozAlign:"left", formatter:"plaintext"}, 31 | {title:"Message", field:"msg", hozAlign:"left", formatter:"textarea"}, 32 | {title:"Value", field:"value", hozAlign:"left", formatter:"plaintext"}, 33 | ], 34 | }); 35 | 36 | refresh_event_table(); 37 | } 38 | 39 | function refresh_event_table(){ 40 | socket.emit('get_event_table', {} ); 41 | } 42 | -------------------------------------------------------------------------------- /client/static/client-gis.js: -------------------------------------------------------------------------------- 1 | var gis_in_view; 2 | 3 | function init_gis(){ 4 | gis_in_view = []; 5 | 6 | leafletmap = L.map('mmi_svg', { 7 | renderer: L.svg(), 8 | mapType: "gis" 9 | }).setView([51.99461, 5.82955], 17); 10 | //gis_map = new L.Map('gis_map').setView([51.980, 5.842], 17); 11 | L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 12 | attribution: 'openstreetmap', 13 | maxZoom: 24, 14 | id: 'openstreetmap', 15 | tileSize: 512, 16 | zoomOffset: -1, 17 | }).addTo(leafletmap);//*/ 18 | 19 | //register svg events 20 | leafletmap.on(L.Draw.Event.CREATED, gis_addItem);//*/ 21 | leafletmap.on(L.Draw.Event.EDITED, gis_editedItems); 22 | leafletmap.on(L.Draw.Event.DELETED, gis_removeItems); 23 | leafletmap.on('moveend', update_gis); 24 | leafletmap.on('zoomend', update_gis); 25 | 26 | //ensure layers are not updated from database during editing or removing 27 | leafletmap.on('draw:editstart', function(){ leafletmap.off('moveend', update_gis); leafletmap.off('zoomend', update_gis);} ); 28 | leafletmap.on('draw:editstop', function(){leafletmap.on('moveend', update_gis); leafletmap.on('zoomend', update_gis);} ); 29 | leafletmap.on('draw:deletestart', function(){leafletmap.off('moveend', update_gis); leafletmap.off('zoomend', update_gis);} ); 30 | leafletmap.on('draw:deletestop', function(){leafletmap.on('moveend', update_gis); leafletmap.on('zoomend', update_gis);} ); 31 | 32 | if(location.hash === ""){ //ensure were not setting a location in the url 33 | leafletmap.setZoom(18); 34 | } 35 | 36 | 37 | 38 | socket.on('svg_object_add_to_gis', function (data) { 39 | //add svg to object 40 | if(gis_in_view.includes(data['id'])){ 41 | return; 42 | } 43 | let layer = svg_add_to_gis(data['svg'],data['id'],data['location']); 44 | if(layer == null){ 45 | return; 46 | } 47 | layer.on("click", show_Sidebar); 48 | 49 | if('properties' in data){ 50 | layer._dataPoints = data["properties"]['datapoints']; 51 | layer.properties = data["properties"]; 52 | //find svg elements that need instantiation in list of overrides 53 | try { 54 | if("overrides" in data["properties"]){ 55 | data["properties"]['overrides'].forEach((override) => { 56 | overridable_instance = override["element_id"]; 57 | overridable_property = override["property"]; 58 | instance_value = override["value"]; 59 | $(layer._image).find("*").each(function(idx, el){ 60 | if(el.id == overridable_instance){ 61 | el[overridable_property] = instance_value; 62 | } 63 | }); 64 | }); 65 | } 66 | } 67 | catch (error) { 68 | console.log("error processing overrides:" + error) 69 | } 70 | } 71 | 72 | layer._image.layerNode = layer;//for onclick events in svg, to find the node back 73 | gis_in_view.push(data['id']); 74 | for (const [key, point] of Object.entries(layer._dataPoints)) { 75 | for (const [child_key, child_point] of Object.entries(point)) { 76 | socket.emit('register_datapoint', child_key); 77 | local_data_cache_norefresh[child_key] = false; 78 | } 79 | } 80 | }); 81 | 82 | socket.on('svg_object_remove_from_gis', function (data) { 83 | //remove svg from object 84 | let layer = null 85 | for(let item in editableLayers._layers){ 86 | if(editableLayers._layers[item].uuid === data){ 87 | layer = editableLayers._layers[item]; 88 | break; // If you want to break out of the loop once you've found a match 89 | } 90 | } 91 | if(layer != null){ 92 | for (const [key, point] of Object.entries(layer._dataPoints)) { 93 | for (const [child_key, child_point] of Object.entries(point)) { 94 | socket.emit('unregister_datapoint', child_key); 95 | } 96 | } 97 | 98 | editableLayers.removeLayer(layer); 99 | let index = gis_in_view.indexOf(data); 100 | if (index > -1) { 101 | gis_in_view.splice(index, 1); 102 | } 103 | } 104 | }); 105 | 106 | } 107 | 108 | 109 | function gis_addItem(e) { 110 | let type = e.layerType, layer = e.layer; 111 | 112 | if (type === 'svg') { 113 | layer.type = "Svg"; 114 | layer.on("click", show_Sidebar); 115 | 116 | if(layer._newTemplate == true){ 117 | socket.emit('svg_addTemplate', { 118 | 'name':layer._templateName, 119 | 'viewBox': layer._svgViewBox, 120 | 'svg': layer._url.innerHTML, 121 | 'datapoint_amount':layer._dataPoints.length 122 | }); 123 | } 124 | let latlng = layer.getLatLng(); 125 | let bounds = layer.getBounds(); 126 | socket.emit('gis_addItem', { 127 | 'type': "Svg", 128 | "location": { "type": "Point", 129 | "coordinates": [ latlng.lng, latlng.lat ], //coords are switched position from leaflet in geojson 130 | "height": bounds._northEast.lat - bounds._southWest.lat, 131 | "width": bounds._northEast.lng - bounds._southWest.lng 132 | }, 133 | "properties": { 134 | 'svg':layer._templateName, 135 | 'datapoints': layer._dataPoints 136 | } 137 | }, 138 | function(ret){ 139 | if (!layer.uuid){ 140 | layer.uuid = ret; 141 | } 142 | } 143 | ); 144 | } 145 | else if (layer.toGeoJSON){ //geojson item 146 | layer.type = "Feature"; 147 | layer._dataPoints = {}; 148 | layer.on("click", show_Sidebar); 149 | 150 | layer.feature = {"properties":{}}; 151 | layer.feature.properties['fillEnabled'] = layer.options.fill;//true/false 152 | layer.feature.properties['fill'] = layer.options.fillColor; 153 | layer.feature.properties['fill-opacity'] = layer.options.fillOpacity; 154 | layer.feature.properties["fillRule"] = layer.options.fillRule; 155 | layer.feature.properties['strokeEnabled' ] = layer.options.stroke;//true/false 156 | layer.feature.properties['stroke'] = layer.options.color; 157 | layer.feature.properties['stroke-width'] = layer.options.weight; 158 | layer.feature.properties['stroke-opacity'] = layer.options.opacity; 159 | layer.feature.properties['stroke-cap'] = layer.options.lineCap;// "round", 160 | layer.feature.properties['stroke-join'] = layer.options.lineJoin;// "round", 161 | layer.feature.properties['stroke-dashArray'] = layer.options.dashArray;// null, 162 | layer.feature.properties['stroke-dashOffset'] = layer.options.dashOffset;// null, 163 | layer.feature.properties['smoothFactor'] = layer.options.smoothFactor;//": 1, 164 | layer.feature.properties['noClip'] = layer.options.noClip;// false, 165 | 166 | layer.feature["properties"]['datapoints'] = layer._dataPoints; 167 | 168 | let geojson = layer.toGeoJSON(); 169 | if (layer instanceof L.Circle) { //add circles to geojson 170 | geojson.properties.radius = layer.getRadius(); 171 | } 172 | 173 | geojson['type'] = layer.type; 174 | socket.emit('gis_addItem', geojson , 175 | function(ret){ 176 | if (!layer.uuid){ 177 | layer.uuid = ret; 178 | } 179 | }); 180 | } 181 | editableLayers.addLayer(layer); 182 | } 183 | 184 | 185 | 186 | function gis_editedItems(e){ 187 | for (const [key, layer] of Object.entries(e.layers._layers)) { 188 | if(layer.type && layer.type === "Feature"){ 189 | let geojson = layer.toGeoJSON(); 190 | if (layer instanceof L.Circle) { //add circles to geojson 191 | geojson.properties.radius = layer.getRadius(); 192 | } 193 | 194 | geojson["properties"]['datapoints'] = layer._dataPoints; 195 | 196 | socket.emit('gis_editedItems', { 197 | '_id':layer.uuid, 198 | 'type': layer.type, 199 | 'geometry':geojson['geometry'], 200 | 'properties':geojson['properties'], 201 | }); 202 | } 203 | else if(layer.type && layer.type === "Svg"){ 204 | let latlng = layer.getLatLng(); 205 | let bounds = layer.getBounds(); 206 | layer.properties['datapoints'] = layer._dataPoints; 207 | 208 | socket.emit('gis_editedItems', { 209 | '_id':layer.uuid, 210 | 'type': layer.type, 211 | "location": { "type": "Point", 212 | "coordinates": [ latlng.lng, latlng.lat ], //coords are switched position from leaflet in geojson 213 | "height": bounds._northEast.lat - bounds._southWest.lat, 214 | "width": bounds._northEast.lng - bounds._southWest.lng, 215 | }, 216 | "properties": layer.properties, 217 | }); 218 | } 219 | } 220 | } 221 | 222 | 223 | 224 | function gis_removeItems(e){ 225 | for (const [key, layer] of Object.entries(e.layers._layers)) { 226 | socket.emit('gis_removeItems', layer.uuid); 227 | } 228 | } 229 | 230 | 231 | var update_gis = function(){ 232 | zoom = leafletmap.getZoom(); 233 | bounds = leafletmap.getBounds(); 234 | socket.emit('get_objects_for_gis', {'w': bounds.getWest(), 'n': bounds.getNorth(), 'e': bounds.getEast(), 's': bounds.getSouth(), 'z': zoom, 'in_view': gis_in_view}); 235 | } 236 | 237 | 238 | function svg_add_to_gis(svgString, svgId, location) { 239 | // correct leaflet size+latlng based on gis coordinate system (by ajusting bounds) 240 | let hheight = location['height']/2; 241 | let hwidth = location['width']/2; 242 | let longtitude = location['coordinates'][0]; 243 | let latitude = location['coordinates'][1]; 244 | 245 | // set svg viewbox with x,y,x2,y2 (i.e. 0,0,512,512). as a side-effect, the leaflet bounds on map are also set to this by default 246 | let svg = new L.SvgObject( 247 | svgString, 248 | L.latLngBounds([ [ latitude-hheight,longtitude-hwidth], [ latitude+hheight,longtitude+hwidth ] ]), 249 | svgId, 250 | { svgViewBox:{ viewBox: "calculate", fitBounds: false, scaleBounds: 0.000005 }} 251 | ); 252 | svg.type = "Svg"; 253 | // add to gis layer 254 | editableLayers.addLayer(svg); 255 | return svg; 256 | } 257 | 258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /client/static/client-schema.js: -------------------------------------------------------------------------------- 1 | var schema_in_view; 2 | 3 | //we use an inverted Y axis for this non geographic coordinate reference system 4 | //https://stackoverflow.com/questions/62305306/invert-y-axis-of-lcrs-simple-map-on-vue2-leaflet 5 | var CRSPixel = L.Util.extend(L.CRS.Simple, { 6 | transformation: new L.Transformation(1,0,1,0) 7 | }); 8 | 9 | function init_schema(){ 10 | schema_in_view = []; 11 | 12 | leafletmap = L.map('mmi_svg', { 13 | renderer: L.svg(), 14 | crs: CRSPixel, //non geographic map with inverted Y axis 15 | minZoom: 0,//-5 16 | maxZoom: 22, 17 | mapType: "schema" 18 | }).setView([0, 0], 17);//setView([0,0], 1); 19 | 20 | //register events for map display and edits 21 | leafletmap.on(L.Draw.Event.CREATED, schema_addItem); 22 | leafletmap.on(L.Draw.Event.EDITED, schema_editedItems); 23 | leafletmap.on(L.Draw.Event.DELETED, schema_removeItems); 24 | leafletmap.on('moveend', update_schema); 25 | leafletmap.on('zoomend', update_schema); 26 | 27 | //do not update map when editing, to prevent layer modification from database during editing 28 | leafletmap.on('draw:editstart', function(){ leafletmap.off('moveend', update_schema); leafletmap.off('zoomend', update_schema);} ); 29 | leafletmap.on('draw:editstop', function(){leafletmap.on('moveend', update_schema); leafletmap.on('zoomend', update_schema);} ); 30 | leafletmap.on('draw:deletestart', function(){leafletmap.off('moveend', update_schema); leafletmap.off('zoomend', update_schema);} ); 31 | leafletmap.on('draw:deletestop', function(){leafletmap.on('moveend', update_schema); leafletmap.on('zoomend', update_schema);} ); 32 | 33 | if(location.hash === ""){ //ensure we're not setting a location in the url 34 | leafletmap.setZoom(18); 35 | } 36 | 37 | //add svg to object 38 | socket.on('svg_object_add_to_schema', function (data) { 39 | //bail if object already in view 40 | if(schema_in_view.includes(data['id'])){ 41 | return; 42 | } 43 | //add svg to leaflet map 44 | let layer = svg_add_to_schema(data['w'],data['n'],data['e'],data['s'],data['svg'],data['id']); 45 | if(layer == null){ //dont continue if object could not be added 46 | return; 47 | } 48 | 49 | if('properties' in data){ 50 | layer.properties = data["properties"]; 51 | layer._dataPoints = data["properties"]['datapoints']; 52 | //find svg elements that need instantiation in list of overrides 53 | try { 54 | if("overrides" in data["properties"]){ 55 | data["properties"]['overrides'].forEach((override) => { 56 | overridable_instance = override["element_id"]; 57 | overridable_property = override["property"]; 58 | instance_value = override["value"]; 59 | $(layer._image).find("*").each(function(idx, el){ 60 | if(el.id == overridable_instance){ 61 | el[overridable_property] = instance_value; 62 | } 63 | }); 64 | }); 65 | } 66 | } 67 | catch (error) { 68 | console.log("error processing overrides:" + error) 69 | } 70 | 71 | } 72 | 73 | //register click event 74 | layer.on("click", show_Sidebar); 75 | layer._image.layerNode = layer;//reference for onclick events in svg, to find the node back 76 | schema_in_view.push(data['id']);//maintain a list of existing objects in view 77 | //register datapoints for updating from server 78 | for (const [key, point] of Object.entries(layer._dataPoints)) { 79 | for (const [child_key, child_point] of Object.entries(point)) { 80 | socket.emit('register_datapoint', child_key); 81 | local_data_cache_norefresh[child_key] = false; 82 | } 83 | } 84 | }); 85 | 86 | socket.on('svg_object_remove_from_schema', function (data) { 87 | //remove svg from object 88 | let layer = null 89 | for(let item in editableLayers._layers){ 90 | if(editableLayers._layers[item].uuid === data){ 91 | layer = editableLayers._layers[item]; 92 | break; 93 | } 94 | } 95 | if(layer != null){ 96 | //unregister values in this svg 97 | for (const [key, point] of Object.entries(layer._dataPoints)) { 98 | for (const [child_key, child_point] of Object.entries(point)) { 99 | socket.emit('unregister_datapoint', child_key); 100 | } 101 | } 102 | editableLayers.removeLayer(layer); 103 | let index = schema_in_view.indexOf(data); 104 | if (index > -1) { 105 | schema_in_view.splice(index, 1); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | 112 | //function if item(svg/geojson) is added via the editor 113 | function schema_addItem(e) { 114 | let type = e.layerType, layer = e.layer; 115 | 116 | if (type === 'svg') { 117 | layer.type = "Svg"; 118 | layer.on("click", show_Sidebar); 119 | 120 | if(layer._newTemplate == true){ 121 | socket.emit('svg_addTemplate', { 122 | 'name':layer._templateName, 123 | 'viewBox': layer._svgViewBox, 124 | 'svg': layer._url.innerHTML, 125 | 'datapoint_amount':layer._dataPoints.length //TODO: is this even used? 126 | }); 127 | } 128 | 129 | let bounds = layer.getBounds(); 130 | layer.properties = layer.options; 131 | 132 | layer.properties['datapoints'] = layer._dataPoints; 133 | socket.emit('schema_addItems', { 134 | 'w':bounds._northEast.lng, 135 | 'n':bounds._northEast.lat, 136 | 'e':bounds._southWest.lng, 137 | 's':bounds._southWest.lat, 138 | 'svg':layer._templateName, 139 | "properties": layer.properties, 140 | }, 141 | function(ret){ 142 | if (!layer.uuid){ 143 | layer.uuid = ret; 144 | } 145 | } 146 | ); 147 | } 148 | else if (layer.toGeoJSON){ //geojson item 149 | layer.type = "Feature"; 150 | layer._dataPoints = {}; 151 | layer.on("click", show_Sidebar); 152 | 153 | layer.feature = {"properties":{}}; 154 | layer.feature.properties['fillEnabled'] = layer.options.fill;//true/false 155 | layer.feature.properties['fill'] = layer.options.fillColor; 156 | layer.feature.properties['fill-opacity'] = layer.options.fillOpacity; 157 | layer.feature.properties["fillRule"] = layer.options.fillRule; 158 | layer.feature.properties['strokeEnabled' ] = layer.options.stroke;//true/false 159 | layer.feature.properties['stroke'] = layer.options.color; 160 | layer.feature.properties['stroke-width'] = layer.options.weight; 161 | layer.feature.properties['stroke-opacity'] = layer.options.opacity; 162 | layer.feature.properties['stroke-cap'] = layer.options.lineCap;// "round", 163 | layer.feature.properties['stroke-join'] = layer.options.lineJoin;// "round", 164 | layer.feature.properties['stroke-dashArray'] = layer.options.dashArray;// null, 165 | layer.feature.properties['stroke-dashOffset'] = layer.options.dashOffset;// null, 166 | layer.feature.properties['smoothFactor'] = layer.options.smoothFactor;//": 1, 167 | layer.feature.properties['noClip'] = layer.options.noClip;// false, 168 | 169 | layer.feature.properties['datapoints'] = layer._dataPoints; 170 | 171 | let geojson = layer.toGeoJSON(); 172 | if (layer instanceof L.Circle) { //add circles to geojson 173 | geojson.properties.radius = layer.getRadius(); 174 | } 175 | 176 | geojson['type'] = layer.type; 177 | socket.emit('schema_addGeojsonItem', geojson , 178 | function(ret){ 179 | if (!layer.uuid){ 180 | layer.uuid = ret; 181 | } 182 | }); 183 | } 184 | editableLayers.addLayer(layer); 185 | } 186 | 187 | 188 | 189 | function schema_editedItems(e){ 190 | for (const [key, layer] of Object.entries(e.layers._layers)) { 191 | if(layer.type && layer.type === "Feature"){ 192 | let geojson = layer.toGeoJSON(); 193 | if (layer instanceof L.Circle) { //add circles to geojson 194 | geojson.properties.radius = layer.getRadius(); 195 | } 196 | 197 | geojson["properties"]['datapoints'] = layer._dataPoints; 198 | 199 | socket.emit('schema_editedGeojsonItems', { 200 | '_id':layer.uuid, 201 | 'type': layer.type, 202 | 'geometry':geojson['geometry'], 203 | 'properties':geojson['properties'], 204 | }); 205 | } 206 | else if(layer.type && layer.type === "Svg"){ 207 | let bounds = layer.getBounds(); 208 | layer.properties['datapoints'] = layer._dataPoints; 209 | 210 | socket.emit('schema_editedItems', { 211 | '_id':layer.uuid, 212 | 'w':bounds._northEast.lng, 213 | 'n':bounds._northEast.lat, 214 | 'e':bounds._southWest.lng, 215 | 's':bounds._southWest.lat, 216 | "properties": layer.properties, 217 | }); 218 | } 219 | } 220 | } 221 | 222 | 223 | function schema_removeItems(e){ 224 | for (const [key, layer] of Object.entries(e.layers._layers)) { 225 | if(layer.type && layer.type === "Feature"){ 226 | socket.emit('schema_removeGeojsonItems', layer.uuid); 227 | } 228 | else if(layer.type && layer.type === "Svg"){ 229 | socket.emit('schema_removeItems', layer.uuid); 230 | } 231 | } 232 | } 233 | 234 | var update_schema = function(){ 235 | zoom = leafletmap.getZoom(); 236 | bounds = leafletmap.getBounds(); 237 | //warning, due to different coordinate system, north and south are reversed 238 | socket.emit('get_objects_for_schema', {'w': bounds.getWest(), 'n': bounds.getSouth(), 'z': zoom, 'in_view': schema_in_view, 'e': bounds.getEast(), 's': bounds.getNorth()}); 239 | } 240 | 241 | 242 | function svg_add_to_schema(w, n, e, s, svgString, svgId) { 243 | let svg = new L.SvgObject(svgString, L.latLngBounds([[n,w],[s,e]]), svgId,{ svgViewBox:{ viewBox: "calculate", fitBounds: false, scaleBounds: 0.000005/*1.0*/ }}); 244 | svg.type = "Svg"; 245 | editableLayers.addLayer(svg); 246 | return svg; 247 | } 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /client/static/css/L.Control.Sidebar.css: -------------------------------------------------------------------------------- 1 | .leaflet-sidebar { 2 | position: absolute; 3 | height: 100%; 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | padding: 10px; 8 | z-index: 2000; } 9 | .leaflet-sidebar.left { 10 | left: -500px; 11 | transition: left 0.5s, width 0.5s; 12 | padding-right: 0; } 13 | .leaflet-sidebar.left.visible { 14 | left: 0; } 15 | .leaflet-sidebar.right { 16 | right: -500px; 17 | transition: right 0.5s, width 0.5s; 18 | padding-left: 0; } 19 | .leaflet-sidebar.right.visible { 20 | right: 0; } 21 | .leaflet-sidebar > .leaflet-control { 22 | height: 100%; 23 | width: 100%; 24 | overflow: auto; 25 | -webkit-overflow-scrolling: touch; 26 | -webkit-box-sizing: border-box; 27 | -moz-box-sizing: border-box; 28 | box-sizing: border-box; 29 | padding: 8px 24px; 30 | font-size: 1.1em; 31 | background: white; 32 | box-shadow: 0 1px 7px rgba(0, 0, 0, 0.65); 33 | -webkit-border-radius: 4px; 34 | border-radius: 4px; } 35 | .leaflet-touch .leaflet-sidebar > .leaflet-control { 36 | box-shadow: none; 37 | border: 2px solid rgba(0, 0, 0, 0.2); 38 | background-clip: padding-box; } 39 | @media (max-width: 767px) { 40 | .leaflet-sidebar { 41 | width: 100%; 42 | padding: 0; } 43 | .leaflet-sidebar.left.visible ~ .leaflet-left { 44 | left: 100%; } 45 | .leaflet-sidebar.right.visible ~ .leaflet-right { 46 | right: 100%; } 47 | .leaflet-sidebar.left { 48 | left: -100%; } 49 | .leaflet-sidebar.left.visible { 50 | left: 0; } 51 | .leaflet-sidebar.right { 52 | right: -100%; } 53 | .leaflet-sidebar.right.visible { 54 | right: 0; } 55 | .leaflet-sidebar > .leaflet-control { 56 | box-shadow: none; 57 | -webkit-border-radius: 0; 58 | border-radius: 0; } 59 | .leaflet-touch .leaflet-sidebar > .leaflet-control { 60 | border: 0; } } 61 | @media (min-width: 768px) and (max-width: 991px) { 62 | .leaflet-sidebar { 63 | width: 305px; } 64 | .leaflet-sidebar.left.visible ~ .leaflet-left { 65 | left: 305px; } 66 | .leaflet-sidebar.right.visible ~ .leaflet-right { 67 | right: 305px; } } 68 | @media (min-width: 992px) and (max-width: 1199px) { 69 | .leaflet-sidebar { 70 | width: 390px; } 71 | .leaflet-sidebar.left.visible ~ .leaflet-left { 72 | left: 390px; } 73 | .leaflet-sidebar.right.visible ~ .leaflet-right { 74 | right: 390px; } } 75 | @media (min-width: 1200px) { 76 | .leaflet-sidebar { 77 | width: 460px; } 78 | .leaflet-sidebar.left.visible ~ .leaflet-left { 79 | left: 460px; } 80 | .leaflet-sidebar.right.visible ~ .leaflet-right { 81 | right: 460px; } } 82 | .leaflet-sidebar .close { 83 | position: absolute; 84 | right: 20px; 85 | top: 20px; 86 | width: 31px; 87 | height: 31px; 88 | color: #333; 89 | font-size: 25px; 90 | line-height: 1em; 91 | text-align: center; 92 | background: white; 93 | -webkit-border-radius: 16px; 94 | border-radius: 16px; 95 | cursor: pointer; 96 | z-index: 1000; } 97 | 98 | .leaflet-left { 99 | transition: left 0.5s; } 100 | 101 | .leaflet-right { 102 | transition: right 0.5s; } 103 | -------------------------------------------------------------------------------- /client/static/css/L.Control.Sidebar.scss: -------------------------------------------------------------------------------- 1 | $threshold-lg: 1200px; 2 | $threshold-md: 992px; 3 | $threshold-sm: 768px; 4 | 5 | $width-lg: 460px; 6 | $width-md: 390px; 7 | $width-sm: 305px; 8 | $width-xs: 100%; 9 | 10 | $transition-duration: 0.5s; 11 | 12 | @mixin border-radius($border-radius) { 13 | -webkit-border-radius: $border-radius; 14 | border-radius: $border-radius; 15 | } 16 | 17 | @mixin box-sizing($box-sizing) { 18 | -webkit-box-sizing: $box-sizing; 19 | -moz-box-sizing: $box-sizing; 20 | box-sizing: $box-sizing; 21 | } 22 | 23 | @mixin widths($width) { 24 | width: $width; 25 | 26 | &.left.visible ~ .leaflet-left { 27 | left: $width; 28 | } 29 | 30 | &.right.visible ~ .leaflet-right { 31 | right: $width; 32 | } 33 | } 34 | 35 | .leaflet-sidebar { 36 | position: absolute; 37 | height: 100%; 38 | 39 | @include box-sizing(border-box); 40 | padding: 10px; 41 | 42 | z-index: 2000; 43 | 44 | &.left { 45 | left: -500px; 46 | transition: left $transition-duration, width $transition-duration; 47 | 48 | padding-right: 0; 49 | 50 | &.visible { 51 | left: 0; 52 | } 53 | } 54 | 55 | &.right { 56 | right: -500px; 57 | transition: right $transition-duration, width $transition-duration; 58 | 59 | padding-left: 0; 60 | 61 | &.visible { 62 | right: 0; 63 | } 64 | } 65 | 66 | & > .leaflet-control { 67 | height: 100%; 68 | width: 100%; 69 | 70 | overflow: auto; 71 | -webkit-overflow-scrolling: touch; 72 | 73 | @include box-sizing(border-box); 74 | padding: 8px 24px; 75 | 76 | font-size: 1.1em; 77 | 78 | background: white; 79 | box-shadow: 0 1px 7px rgba(0,0,0,0.65); 80 | @include border-radius(4px); 81 | 82 | .leaflet-touch & { 83 | box-shadow: none; 84 | border: 2px solid rgba(0,0,0,0.2); 85 | background-clip: padding-box; 86 | } 87 | } 88 | 89 | @media(max-width:$threshold-sm - 1px) { 90 | @include widths($width-xs); 91 | 92 | padding: 0; 93 | 94 | &.left { 95 | left: -$width-xs; 96 | 97 | &.visible { 98 | left: 0; 99 | } 100 | } 101 | 102 | &.right { 103 | right: -$width-xs; 104 | 105 | &.visible { 106 | right: 0; 107 | } 108 | } 109 | 110 | & > .leaflet-control { 111 | box-shadow: none; 112 | @include border-radius(0); 113 | 114 | .leaflet-touch & { 115 | border: 0; 116 | } 117 | } 118 | } 119 | 120 | @media(min-width:$threshold-sm) and (max-width:$threshold-md - 1px) { 121 | @include widths($width-sm); 122 | } 123 | 124 | @media(min-width:$threshold-md) and (max-width:$threshold-lg - 1px) { 125 | @include widths($width-md); 126 | } 127 | 128 | @media(min-width:$threshold-lg) { 129 | @include widths($width-lg); 130 | } 131 | 132 | .close { 133 | position: absolute; 134 | right: 20px; 135 | top: 20px; 136 | width: 31px; 137 | height: 31px; 138 | 139 | color: #333; 140 | font-size: 25pt; 141 | line-height: 1em; 142 | text-align: center; 143 | background: white; 144 | @include border-radius(16px); 145 | cursor: pointer; 146 | 147 | // https://github.com/Turbo87/leaflet-sidebar/issues/36 148 | z-index: 1000; 149 | } 150 | } 151 | 152 | .leaflet-left { 153 | transition: left $transition-duration; 154 | } 155 | 156 | .leaflet-right { 157 | transition: right $transition-duration; 158 | } 159 | -------------------------------------------------------------------------------- /client/static/css/easy-button.css: -------------------------------------------------------------------------------- 1 | .leaflet-bar button, 2 | .leaflet-bar button:hover { 3 | background-color: #fff; 4 | border: none; 5 | border-bottom: 1px solid #ccc; 6 | width: 26px; 7 | height: 26px; 8 | line-height: 26px; 9 | display: block; 10 | text-align: center; 11 | text-decoration: none; 12 | color: black; 13 | } 14 | 15 | .leaflet-bar button { 16 | background-position: 50% 50%; 17 | background-repeat: no-repeat; 18 | overflow: hidden; 19 | display: block; 20 | } 21 | 22 | .leaflet-bar button:hover { 23 | background-color: #f4f4f4; 24 | } 25 | 26 | .leaflet-bar button:first-of-type { 27 | border-top-left-radius: 4px; 28 | border-top-right-radius: 4px; 29 | } 30 | 31 | .leaflet-bar button:last-of-type { 32 | border-bottom-left-radius: 4px; 33 | border-bottom-right-radius: 4px; 34 | border-bottom: none; 35 | } 36 | 37 | .leaflet-bar.disabled, 38 | .leaflet-bar button.disabled { 39 | cursor: default; 40 | pointer-events: none; 41 | opacity: .4; 42 | } 43 | 44 | .easy-button-button .button-state{ 45 | display: block; 46 | width: 100%; 47 | height: 100%; 48 | position: relative; 49 | } 50 | 51 | 52 | .leaflet-touch .leaflet-bar button { 53 | width: 30px; 54 | height: 30px; 55 | line-height: 26px; 56 | } 57 | -------------------------------------------------------------------------------- /client/static/css/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/layers-2x.png -------------------------------------------------------------------------------- /client/static/css/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/layers.png -------------------------------------------------------------------------------- /client/static/css/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/marker-icon-2x.png -------------------------------------------------------------------------------- /client/static/css/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/marker-icon.png -------------------------------------------------------------------------------- /client/static/css/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/marker-shadow.png -------------------------------------------------------------------------------- /client/static/css/images/spritesheet-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/spritesheet-2x.png -------------------------------------------------------------------------------- /client/static/css/images/spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/spritesheet.png -------------------------------------------------------------------------------- /client/static/css/images/spritesheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 157 | -------------------------------------------------------------------------------- /client/static/css/images/svg-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/svg-icon-2x.png -------------------------------------------------------------------------------- /client/static/css/images/svg-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robidev/open_scada_dms/c4baabee20479956cdbb631de49c777a3912fd89/client/static/css/images/svg-icon.png -------------------------------------------------------------------------------- /client/static/css/images/svg-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /client/static/css/layout.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | ::-webkit-scrollbar-track 6 | { 7 | border: 1px solid #1d272f; 8 | background-color: #1d272f; 9 | } 10 | 11 | ::-webkit-scrollbar 12 | { 13 | width: 10px; 14 | background-color: #1d272f; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb 18 | { 19 | background-color: #AAA; 20 | } 21 | 22 | ::-webkit-scrollbar-corner { 23 | background: #1d272f; 24 | } 25 | 26 | ::-webkit-resizer { 27 | background: #1d272f; 28 | } 29 | 30 | #mmi_svg { 31 | background: rgb(0, 0, 0); 32 | } 33 | 34 | pre { 35 | padding: 0; 36 | margin: 0; 37 | font-size: 12px; 38 | line-height: 12px; 39 | } 40 | 41 | h2.teal { 42 | color: #26343e; 43 | background-color: #00b9a0; 44 | } 45 | h2.magenta { 46 | color: #26343e; 47 | background-color: rgba(255, 0, 255, 0.692); 48 | } 49 | 50 | h2 { 51 | margin: 0px; 52 | padding-left: 10px; 53 | line-height: 36px; 54 | font-size: 16px; 55 | font-weight: bold; 56 | /*margin-bottom: 5px;*/ 57 | } 58 | 59 | body { 60 | margin: 0; 61 | font: 12px/1.5 Arial, Helvetica, sans-serif; 62 | background: #1d272f; 63 | color: #FFFFFF; 64 | text-align: left; 65 | scrollbar-color: #AAA #1d272f; 66 | } 67 | 68 | .wrap { 69 | width: auto; 70 | 71 | display: grid; 72 | grid-template-columns: auto;/*300px auto; /* 100px auto 200px*/ 73 | grid-template-rows: auto 600px; 74 | height: 100vh; 75 | } 76 | 77 | /* Create three unequal columns that floats next to each other */ 78 | .column { 79 | /*float: left;*/ 80 | display: grid; 81 | 82 | border: 1px solid #00b9a0; 83 | margin-top: 10px; 84 | margin-bottom: 20px; 85 | margin-left: 10px; 86 | margin-right: 10px; 87 | } 88 | 89 | /* Left column*/ 90 | .column.leftside { 91 | border: 1px solid rgba(255, 0, 255, 0.692); 92 | display: inline; 93 | grid-column: 1; 94 | } 95 | 96 | /* Middle column */ 97 | .column.middle { 98 | display: inline; 99 | grid-column: 1; /* 2 */ 100 | background-color: #009b86; 101 | } 102 | 103 | /* right column */ 104 | .column.rightside { 105 | display: inline; 106 | grid-column: 3; /* 2 */ 107 | resize: both; 108 | overflow: auto; 109 | /*display: none;*/ 110 | } 111 | 112 | .content { 113 | padding-top: 0px; 114 | padding-bottom: 2px; 115 | padding-left: 2px; 116 | padding-right: 2px; 117 | } 118 | 119 | /* Style the footer */ 120 | .footer { 121 | text-align: left; 122 | border: 1px solid rgba(255, 0, 255, 0.692); 123 | margin-bottom: 20px; 124 | margin-left: 10px; 125 | margin-right: 10px; 126 | padding: 10px; 127 | padding-bottom: 16px; 128 | 129 | grid-row: 2; 130 | grid-column-start: 1; 131 | grid-column-end: 3;/* 4 */ 132 | 133 | display: grid; 134 | grid-template-rows: auto 52px; 135 | 136 | resize: both; 137 | overflow: auto; 138 | } 139 | 140 | /* Style the tab */ 141 | .tab { 142 | overflow: hidden; 143 | grid-row: 2; 144 | } 145 | 146 | 147 | /* Style the buttons that are used to open the tab content */ 148 | .tab button { 149 | background-color: inherit; 150 | float: left; 151 | border: none; 152 | outline: none; 153 | cursor: pointer; 154 | padding: 7px 8px; 155 | transition: 0.3s; 156 | } 157 | 158 | /* Middle column */ 159 | .mapButton { 160 | color: #26343e; 161 | background-color: #009b86; 162 | text-decoration:none; 163 | border: none; 164 | outline: none; 165 | padding: 7px 8px; 166 | transition: 0.3s; 167 | font-size: 16px; 168 | font-weight: bold; 169 | 170 | display: inline-block; 171 | } 172 | 173 | .mapButton:hover { 174 | background-color: #00b9a0; 175 | } 176 | 177 | .mapButton:active { 178 | transition: 0s; 179 | background-color: #00e4c5; 180 | } 181 | 182 | ul.tabs { 183 | padding: 0px 0; 184 | font-size: 0; 185 | margin: 0; 186 | list-style-type: none; 187 | text-align: left; 188 | 189 | /*used for scroll bar*/ 190 | overflow-x: scroll; 191 | white-space: nowrap; 192 | } 193 | 194 | ul.tabs li { 195 | display: inline-flex; 196 | margin: 0; 197 | margin-right: 3px; 198 | } 199 | 200 | ul.tabs li a { 201 | font: 12px Arial, Helvetica, sans-serif; 202 | color: #FFFFFF; 203 | text-decoration: none; 204 | position: relative; 205 | padding: 7px 8px; 206 | outline: none; 207 | } 208 | 209 | ul.tabs li.selected a, ul.tabs li.selected a:hover { 210 | position: relative; 211 | top: 0px; 212 | font-weight: underline; 213 | padding-right: -5px; 214 | color: #000;/*#FFFFFF;*/ 215 | background: #aaa; 216 | } 217 | 218 | /* Style the tab content */ 219 | div.tabcontents { 220 | padding: 6px 12px; 221 | height: 100%; 222 | background: #000; 223 | grid-row: 1; 224 | overflow-y: auto; 225 | 226 | } 227 | 228 | .closeable { 229 | background-image: url('../img/close.png'); 230 | height: 16px; 231 | width: 16px; 232 | display: inline-block; 233 | position: relative; 234 | right: -4px; 235 | top: 4px; 236 | } 237 | 238 | .tabcontents .unselected { 239 | display: none; 240 | } 241 | 242 | .controlInput { 243 | background-color: #000; 244 | border: none; 245 | outline: none; 246 | padding: 7px 8px; 247 | transition: 0.3s; 248 | color: #FFFFFF; 249 | margin-bottom: 10px; 250 | font-size: 12px; 251 | font-weight: normal; 252 | width: 100%; 253 | } 254 | 255 | .controlBtn { 256 | background-color: rgba(255, 0, 255, 0.192); 257 | border: none; 258 | outline: none; 259 | padding: 7px 8px; 260 | transition: 0.3s; 261 | color: #FFFFFF; 262 | width: 100%; 263 | margin-bottom: 10px; 264 | font-size: 16px; 265 | font-weight: bold; 266 | } 267 | 268 | .controlBtn:hover { 269 | background-color: rgba(255, 0, 255, 0.692); 270 | } 271 | 272 | .controlBtn:active { 273 | transition: 0s; 274 | background-color: #F1F; 275 | } 276 | 277 | 278 | .aModal { 279 | position: absolute; 280 | top: 0; 281 | left: 0; 282 | width: 100%; 283 | height: 100%; 284 | background: #26343e; 285 | opacity: 0.5; 286 | display: none; 287 | } 288 | 289 | .aPop { 290 | position: absolute; 291 | display: none; 292 | } 293 | 294 | .aPop-title { 295 | margin: 0px; 296 | padding-left: 10px; 297 | line-height: 36px; 298 | font-size: 16px; 299 | font-weight: bold; 300 | margin-bottom: 5px; 301 | color: #26343e; 302 | background-color: rgba(255, 0, 255, 0.692); 303 | /* height: 20px; 304 | padding: 5px;*/ 305 | 306 | cursor: move; 307 | } 308 | 309 | .aPop-close { 310 | padding-right: 8px; 311 | float: right; 312 | cursor: pointer; 313 | opacity: 0.2; 314 | filter: alpha(opacity=20); 315 | } 316 | .aPop-close:hover { 317 | opacity: 0.8; 318 | filter: alpha(opacity=80); 319 | } 320 | 321 | .aPop-content { 322 | min-width: 400px; 323 | min-height: 200px; 324 | background: #26343e; 325 | padding: 5px; 326 | margin: 0; 327 | font-size: 12px; 328 | line-height: 12px; 329 | } 330 | 331 | /*Color the table rows*/ 332 | #mmi_svg .tabulator-table .tabulator-row{ 333 | color:#fff; 334 | background-color: #000; 335 | } 336 | 337 | /*Color even rows*/ 338 | #mmi_svg .tabulator-table .tabulator-row:nth-child(even) { 339 | background-color: #444; 340 | } 341 | 342 | 343 | @keyframes blinker_info { 344 | 0% { 345 | background-color: rgb(0, 0, 255); 346 | } 347 | 49% { 348 | background-color: rgb(0, 0, 255); 349 | } 350 | 50% { 351 | background-color: rgb(0, 0, 0); 352 | } 353 | 100% { 354 | background-color: rgb(0, 0, 0); 355 | } 356 | } 357 | 358 | @keyframes blinker_low { 359 | 0% { 360 | background-color: rgb(125, 125, 125); 361 | } 362 | 49% { 363 | background-color: rgb(125, 125, 125); 364 | } 365 | 50% { 366 | background-color: rgb(0, 0, 0); 367 | } 368 | 100% { 369 | background-color: rgb(0, 0, 0); 370 | } 371 | } 372 | 373 | @keyframes blinker_medium { 374 | 0% { 375 | background-color: rgb(255, 0, 195); 376 | } 377 | 49% { 378 | background-color: rgb(255, 0, 195); 379 | } 380 | 50% { 381 | background-color: rgb(0, 0, 0); 382 | } 383 | 100% { 384 | background-color: rgb(0, 0, 0); 385 | } 386 | } 387 | 388 | @keyframes blinker_high { 389 | 0% { 390 | background-color: rgb(255, 0, 0); 391 | } 392 | 49% { 393 | background-color: rgb(255, 0, 0); 394 | } 395 | 50% { 396 | background-color: rgb(0, 0, 0); 397 | } 398 | 100% { 399 | background-color: rgb(0, 0, 0); 400 | } 401 | } 402 | 403 | @keyframes blinker_critical { 404 | 0% { 405 | background-color: rgb(179, 0, 255); 406 | } 407 | 49% { 408 | background-color: rgb(179, 0, 255); 409 | } 410 | 50% { 411 | background-color: rgb(0, 0, 0); 412 | } 413 | 100% { 414 | background-color: rgb(0, 0, 0); 415 | } 416 | } 417 | 418 | 419 | 420 | .modal-fader { 421 | display: none; 422 | position: fixed; 423 | top: 0; 424 | left: 0; 425 | right: 0; 426 | bottom: 0; 427 | width: 100%; 428 | z-index: 99998; 429 | background: rgba(0,0,0,0.200); 430 | } 431 | .modal-fader.active { 432 | display: block; 433 | } 434 | .modal-window { 435 | display: none; 436 | position: absolute; 437 | left: 50%; 438 | transform: translateX(-50%); 439 | z-index: 99999; 440 | color: #000; 441 | background: #fff; 442 | padding: 20px; 443 | border-radius: 5px; 444 | font-family: sans-serif; 445 | top: 50px; 446 | } 447 | .modal-window.active { 448 | display: block; 449 | } 450 | 451 | .modal-window h1, 452 | .modal-window h2, 453 | .modal-window h3, 454 | .modal-window h4, 455 | .modal-window h5, 456 | .modal-window h6 { 457 | margin: 0; 458 | } 459 | 460 | .modal-btn { 461 | background: #36a5a5; 462 | border: none; 463 | color: #fff; 464 | padding: 10px 15px; 465 | box-shadow: none; 466 | border-radius: 3px; 467 | text-decoration: none; 468 | } 469 | 470 | .OperSidebar { 471 | font-size: 18px; 472 | line-height: 18px; 473 | } 474 | 475 | .OperBorder { 476 | border: solid rgb(0, 0, 0); 477 | } 478 | 479 | .Btn { 480 | background: #dadada; 481 | border: none; 482 | color: #000000; 483 | padding: 6px 9px; 484 | margin: 5px; 485 | box-shadow: none; 486 | border-radius: 3px; 487 | text-decoration: none; 488 | font-size: 18px; 489 | line-height: 18px; 490 | } 491 | 492 | .Btn.SelectBtn { 493 | width: 193px; 494 | } 495 | 496 | .Btn.CancelBtn { 497 | width: 193px; 498 | margin: 5px -8px 5px 5px; 499 | } 500 | 501 | .Btn.OperBtn { 502 | width: 400px; 503 | } 504 | 505 | .Btn:hover { 506 | background-color: rgba(149, 149, 149, 0.692); 507 | } 508 | 509 | .Btn:active { 510 | transition: 0s; 511 | background-color: rgb(109, 109, 109); 512 | } -------------------------------------------------------------------------------- /client/static/css/leaflet.draw-src.css: -------------------------------------------------------------------------------- 1 | /* ================================================================== */ 2 | /* Toolbars 3 | /* ================================================================== */ 4 | 5 | .leaflet-draw-section { 6 | position: relative; 7 | } 8 | 9 | .leaflet-draw-toolbar { 10 | margin-top: 12px; 11 | } 12 | 13 | .leaflet-draw-toolbar-top { 14 | margin-top: 0; 15 | } 16 | 17 | .leaflet-draw-toolbar-notop a:first-child { 18 | border-top-right-radius: 0; 19 | } 20 | 21 | .leaflet-draw-toolbar-nobottom a:last-child { 22 | border-bottom-right-radius: 0; 23 | } 24 | 25 | .leaflet-draw-toolbar a { 26 | background-image: url('images/spritesheet.png'); 27 | background-image: linear-gradient(transparent, transparent), url('images/spritesheet.svg'); 28 | background-repeat: no-repeat; 29 | background-size: 300px 30px; 30 | background-clip: padding-box; 31 | } 32 | 33 | .leaflet-retina .leaflet-draw-toolbar a { 34 | background-image: url('images/spritesheet-2x.png'); 35 | background-image: linear-gradient(transparent, transparent), url('images/spritesheet.svg'); 36 | } 37 | 38 | .leaflet-draw a { 39 | display: block; 40 | text-align: center; 41 | text-decoration: none; 42 | } 43 | 44 | .leaflet-draw a .sr-only { 45 | position: absolute; 46 | width: 1px; 47 | height: 1px; 48 | padding: 0; 49 | margin: -1px; 50 | overflow: hidden; 51 | clip: rect(0, 0, 0, 0); 52 | border: 0; 53 | } 54 | 55 | /* ================================================================== */ 56 | /* Toolbar actions menu 57 | /* ================================================================== */ 58 | 59 | .leaflet-draw-actions { 60 | display: none; 61 | list-style: none; 62 | margin: 0; 63 | padding: 0; 64 | position: absolute; 65 | left: 26px; /* leaflet-draw-toolbar.left + leaflet-draw-toolbar.width */ 66 | top: 0; 67 | white-space: nowrap; 68 | } 69 | 70 | .leaflet-touch .leaflet-draw-actions { 71 | left: 32px; 72 | } 73 | 74 | .leaflet-right .leaflet-draw-actions { 75 | right: 26px; 76 | left: auto; 77 | } 78 | 79 | .leaflet-touch .leaflet-right .leaflet-draw-actions { 80 | right: 32px; 81 | left: auto; 82 | } 83 | 84 | .leaflet-draw-actions li { 85 | display: inline-block; 86 | } 87 | 88 | .leaflet-draw-actions li:first-child a { 89 | border-left: none; 90 | } 91 | 92 | .leaflet-draw-actions li:last-child a { 93 | -webkit-border-radius: 0 4px 4px 0; 94 | border-radius: 0 4px 4px 0; 95 | } 96 | 97 | .leaflet-right .leaflet-draw-actions li:last-child a { 98 | -webkit-border-radius: 0; 99 | border-radius: 0; 100 | } 101 | 102 | .leaflet-right .leaflet-draw-actions li:first-child a { 103 | -webkit-border-radius: 4px 0 0 4px; 104 | border-radius: 4px 0 0 4px; 105 | } 106 | 107 | .leaflet-draw-actions a { 108 | background-color: #919187; 109 | border-left: 1px solid #AAA; 110 | color: #FFF; 111 | font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif; 112 | line-height: 28px; 113 | text-decoration: none; 114 | padding-left: 10px; 115 | padding-right: 10px; 116 | height: 28px; 117 | } 118 | 119 | .leaflet-touch .leaflet-draw-actions a { 120 | font-size: 12px; 121 | line-height: 30px; 122 | height: 30px; 123 | } 124 | 125 | .leaflet-draw-actions-bottom { 126 | margin-top: 0; 127 | } 128 | 129 | .leaflet-draw-actions-top { 130 | margin-top: 1px; 131 | } 132 | 133 | .leaflet-draw-actions-top a, 134 | .leaflet-draw-actions-bottom a { 135 | height: 27px; 136 | line-height: 27px; 137 | } 138 | 139 | .leaflet-draw-actions a:hover { 140 | background-color: #A0A098; 141 | } 142 | 143 | .leaflet-draw-actions-top.leaflet-draw-actions-bottom a { 144 | height: 26px; 145 | line-height: 26px; 146 | } 147 | 148 | /* ================================================================== */ 149 | /* Draw toolbar 150 | /* ================================================================== */ 151 | 152 | .leaflet-draw-toolbar .leaflet-draw-draw-polyline { 153 | background-position: -2px -2px; 154 | } 155 | 156 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline { 157 | background-position: 0 -1px; 158 | } 159 | 160 | .leaflet-draw-toolbar .leaflet-draw-draw-polygon { 161 | background-position: -31px -2px; 162 | } 163 | 164 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon { 165 | background-position: -29px -1px; 166 | } 167 | 168 | .leaflet-draw-toolbar .leaflet-draw-draw-rectangle { 169 | background-position: -62px -2px; 170 | } 171 | 172 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle { 173 | background-position: -60px -1px; 174 | } 175 | 176 | .leaflet-draw-toolbar .leaflet-draw-draw-circle { 177 | background-position: -92px -2px; 178 | } 179 | 180 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle { 181 | background-position: -90px -1px; 182 | } 183 | 184 | .leaflet-draw-toolbar .leaflet-draw-draw-marker { 185 | background-position: -122px -2px; 186 | } 187 | 188 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker { 189 | background-position: -120px -1px; 190 | } 191 | 192 | .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker { 193 | background-position: -273px -2px; 194 | } 195 | 196 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker { 197 | background-position: -271px -1px; 198 | } 199 | 200 | /* ================================================================== */ 201 | /* Edit toolbar 202 | /* ================================================================== */ 203 | 204 | .leaflet-draw-toolbar .leaflet-draw-edit-edit { 205 | background-position: -152px -2px; 206 | } 207 | 208 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit { 209 | background-position: -150px -1px; 210 | } 211 | 212 | .leaflet-draw-toolbar .leaflet-draw-edit-remove { 213 | background-position: -182px -2px; 214 | } 215 | 216 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove { 217 | background-position: -180px -1px; 218 | } 219 | 220 | .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled { 221 | background-position: -212px -2px; 222 | } 223 | 224 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled { 225 | background-position: -210px -1px; 226 | } 227 | 228 | .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled { 229 | background-position: -242px -2px; 230 | } 231 | 232 | .leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled { 233 | background-position: -240px -2px; 234 | } 235 | 236 | /* ================================================================== */ 237 | /* Drawing styles 238 | /* ================================================================== */ 239 | 240 | .leaflet-mouse-marker { 241 | background-color: #fff; 242 | cursor: crosshair; 243 | } 244 | 245 | .leaflet-draw-tooltip { 246 | background: rgb(54, 54, 54); 247 | background: rgba(0, 0, 0, 0.5); 248 | border: 1px solid transparent; 249 | -webkit-border-radius: 4px; 250 | border-radius: 4px; 251 | color: #fff; 252 | font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif; 253 | margin-left: 20px; 254 | margin-top: -21px; 255 | padding: 4px 8px; 256 | position: absolute; 257 | visibility: hidden; 258 | white-space: nowrap; 259 | z-index: 6; 260 | } 261 | 262 | .leaflet-draw-tooltip:before { 263 | border-right: 6px solid black; 264 | border-right-color: rgba(0, 0, 0, 0.5); 265 | border-top: 6px solid transparent; 266 | border-bottom: 6px solid transparent; 267 | content: ""; 268 | position: absolute; 269 | top: 7px; 270 | left: -7px; 271 | } 272 | 273 | .leaflet-error-draw-tooltip { 274 | background-color: #F2DEDE; 275 | border: 1px solid #E6B6BD; 276 | color: #B94A48; 277 | } 278 | 279 | .leaflet-error-draw-tooltip:before { 280 | border-right-color: #E6B6BD; 281 | } 282 | 283 | .leaflet-draw-tooltip-single { 284 | margin-top: -12px 285 | } 286 | 287 | .leaflet-draw-tooltip-subtext { 288 | color: #f8d5e4; 289 | } 290 | 291 | .leaflet-draw-guide-dash { 292 | font-size: 1%; 293 | opacity: 0.6; 294 | position: absolute; 295 | width: 5px; 296 | height: 5px; 297 | } 298 | 299 | /* ================================================================== */ 300 | /* Edit styles 301 | /* ================================================================== */ 302 | 303 | .leaflet-edit-marker-selected { 304 | background-color: rgba(254, 87, 161, 0.1); 305 | border: 4px dashed rgba(254, 87, 161, 0.6); 306 | -webkit-border-radius: 4px; 307 | border-radius: 4px; 308 | box-sizing: content-box; 309 | } 310 | 311 | .leaflet-edit-move { 312 | cursor: move; 313 | } 314 | 315 | .leaflet-edit-resize { 316 | cursor: pointer; 317 | } 318 | 319 | /* ================================================================== */ 320 | /* Old IE styles 321 | /* ================================================================== */ 322 | 323 | .leaflet-oldie .leaflet-draw-toolbar { 324 | border: 1px solid #999; 325 | } 326 | -------------------------------------------------------------------------------- /client/static/css/leaflet.draw.css: -------------------------------------------------------------------------------- 1 | .leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')} 2 | .leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block} 3 | .leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px} 4 | .leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px} 5 | .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px} 6 | .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-svg {background-position:-122px -2px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px} 7 | .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px} 8 | .leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px} 9 | .leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box} 10 | .leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999} -------------------------------------------------------------------------------- /client/static/css/leaflet.draw.svg.css: -------------------------------------------------------------------------------- 1 | /* ================================================================== */ 2 | /* Draw toolbar 3 | /* ================================================================== */ 4 | 5 | .leaflet-draw-toolbar .leaflet-draw-draw-svg { 6 | background-image: url('images/svg-icon.png'); 7 | background-image: linear-gradient(transparent, transparent), url('images/svg-icon.svg'); 8 | background-repeat: no-repeat; 9 | background-size: 30px 30px; /* 300px 30px; */ 10 | background-clip: padding-box; 11 | /* background-position: -122px -2px;*/ 12 | } 13 | 14 | .leaflet-retina .leaflet-draw-toolbar .leaflet-draw-draw-svg { 15 | background-image: url('images/svg-icon-2x.png'); 16 | background-image: linear-gradient(transparent, transparent), url('images/svg-icon.svg'); 17 | } 18 | 19 | /* ================================================================== */ 20 | /* Edit styles 21 | /* ================================================================== */ 22 | 23 | .leaflet-edit-svg-selected { 24 | outline:2px dashed rgba(254, 87, 161, 0.6); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /client/static/css/leaflet.modal.css: -------------------------------------------------------------------------------- 1 | /* Mixins *********************************************************************/ 2 | /* Styles *********************************************************************/ 3 | .leaflet-modal { 4 | visibility: hidden; 5 | pointer-events: none; 6 | } 7 | .leaflet-modal .overlay { 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | display: block; 12 | background: #222222; 13 | opacity: 0; 14 | filter: alpha(opacity=0); 15 | z-index: 10001; 16 | -webkit-transition: opacity 0.25s linear; 17 | -o-transition: opacity 0.25s linear; 18 | transition: opacity 0.25s linear; 19 | } 20 | .leaflet-modal .close { 21 | -webkit-appearance: none; 22 | padding: 0; 23 | cursor: pointer; 24 | background: 0 0; 25 | border: 0; 26 | float: right; 27 | font-size: 21px; 28 | font-weight: 700; 29 | line-height: 1; 30 | color: #000; 31 | text-shadow: 0 1px 0 #fff; 32 | opacity: 0.2; 33 | filter: alpha(opacity=20); 34 | } 35 | .leaflet-modal .close:hover { 36 | opacity: 0.8; 37 | filter: alpha(opacity=80); 38 | } 39 | .leaflet-modal .modal { 40 | display: block; 41 | left: auto; 42 | margin-right: auto; 43 | margin-left: auto; 44 | margin-top: auto; 45 | position: relative; 46 | -webkit-box-sizing: border-box; 47 | -moz-box-sizing: border-box; 48 | box-sizing: border-box; 49 | z-index: 10002; 50 | overflow: auto; 51 | height: 100%; 52 | } 53 | .leaflet-modal .modal-content { 54 | padding: 15px; 55 | background: #ffffff; 56 | color:#000; 57 | -webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5); 58 | box-shadow: 0 3px 9px rgba(0,0,0,.5); 59 | -webkit-background-clip: padding-box; 60 | background-clip: padding-box; 61 | border: 1px solid #999; 62 | border: 1px solid rgba(0, 0, 0, 0.2); 63 | border-radius: 4px; 64 | outline: 0; 65 | margin-left: auto; 66 | margin-right: auto; 67 | margin-top: 100%; 68 | margin-bottom: 40px; 69 | max-width: 900px; 70 | -webkit-transition: margin 0.3s linear; 71 | -o-transition: margin 0.3s linear; 72 | transition: margin 0.3s linear; 73 | } 74 | .leaflet-modal.show { 75 | width: 100%; 76 | height: 100%; 77 | visibility: visible; 78 | pointer-events: visible; 79 | } 80 | .leaflet-modal.show .modal-content { 81 | margin-top: 40px; 82 | } 83 | .leaflet-modal.show .overlay { 84 | opacity: 0.6; 85 | filter: alpha(opacity=60); 86 | } 87 | -------------------------------------------------------------------------------- /client/static/css/leaflet.modal.min.css: -------------------------------------------------------------------------------- 1 | .leaflet-modal{visibility:hidden;pointer-events:none}.leaflet-modal .overlay{position:absolute;width:100%;height:100%;display:block;background:#222222;opacity:0;filter:alpha(opacity=0);z-index:10001;-webkit-transition:opacity 0.25s linear;-o-transition:opacity 0.25s linear;transition:opacity 0.25s linear}.leaflet-modal .close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0;float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.leaflet-modal .close:hover{opacity:.8;filter:alpha(opacity=80)}.leaflet-modal .modal{display:block;left:auto;margin-right:auto;margin-left:auto;margin-top:auto;position:relative;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;z-index:10002;overflow:auto;height:100%}.leaflet-modal .modal-content{padding:15px;background:#ffffff;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:4px;outline:0;margin-left:auto;margin-right:auto;margin-top:100%;margin-bottom:40px;max-width:600px;-webkit-transition:margin 0.3s linear;-o-transition:margin 0.3s linear;transition:margin 0.3s linear}.leaflet-modal.show{width:100%;height:100%;visibility:visible;pointer-events:visible}.leaflet-modal.show .modal-content{margin-top:40px}.leaflet-modal.show .overlay{opacity:.6;filter:alpha(opacity=60)} -------------------------------------------------------------------------------- /client/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /client/static/lib/L.Control.Sidebar.js: -------------------------------------------------------------------------------- 1 | L.Control.Sidebar = L.Control.extend({ 2 | 3 | includes: L.Evented.prototype || L.Mixin.Events, 4 | 5 | options: { 6 | closeButton: true, 7 | position: 'left', 8 | autoPan: true, 9 | }, 10 | 11 | initialize: function (placeholder, options) { 12 | L.setOptions(this, options); 13 | 14 | // Find content container 15 | var content = this._contentContainer = L.DomUtil.get(placeholder); 16 | 17 | // Remove the content container from its original parent 18 | if(content.parentNode != undefined){ 19 | content.parentNode.removeChild(content); 20 | } 21 | var l = 'leaflet-'; 22 | 23 | // Create sidebar container 24 | var container = this._container = 25 | L.DomUtil.create('div', l + 'sidebar ' + this.options.position); 26 | 27 | // Style and attach content container 28 | L.DomUtil.addClass(content, l + 'control'); 29 | container.appendChild(content); 30 | 31 | // Create close button and attach it if configured 32 | if (this.options.closeButton) { 33 | var close = this._closeButton = 34 | L.DomUtil.create('a', 'close', container); 35 | close.innerHTML = '×'; 36 | } 37 | }, 38 | 39 | addTo: function (map) { 40 | var container = this._container; 41 | var content = this._contentContainer; 42 | 43 | // Attach event to close button 44 | if (this.options.closeButton) { 45 | var close = this._closeButton; 46 | 47 | L.DomEvent.on(close, 'click', this.hide, this); 48 | } 49 | 50 | L.DomEvent 51 | .on(container, 'transitionend', 52 | this._handleTransitionEvent, this) 53 | .on(container, 'webkitTransitionEnd', 54 | this._handleTransitionEvent, this); 55 | 56 | // Attach sidebar container to controls container 57 | var controlContainer = map._controlContainer; 58 | controlContainer.insertBefore(container, controlContainer.firstChild); 59 | 60 | this._map = map; 61 | 62 | // Make sure we don't drag the map when we interact with the content 63 | var stop = L.DomEvent.stopPropagation; 64 | var fakeStop = L.DomEvent._fakeStop || stop; 65 | L.DomEvent 66 | .on(content, 'contextmenu', stop) 67 | .on(content, 'click', fakeStop) 68 | .on(content, 'mousedown', stop) 69 | .on(content, 'touchstart', stop) 70 | .on(content, 'dblclick', fakeStop) 71 | .on(content, 'mousewheel', stop) 72 | .on(content, 'wheel', stop) 73 | .on(content, 'scroll', stop) 74 | .on(content, 'MozMousePixelScroll', stop); 75 | 76 | return this; 77 | }, 78 | 79 | removeFrom: function (map) { 80 | //if the control is visible, hide it before removing it. 81 | this.hide(); 82 | 83 | var container = this._container; 84 | var content = this._contentContainer; 85 | 86 | // Remove sidebar container from controls container 87 | var controlContainer = map._controlContainer; 88 | controlContainer.removeChild(container); 89 | 90 | //disassociate the map object 91 | this._map = null; 92 | 93 | // Unregister events to prevent memory leak 94 | var stop = L.DomEvent.stopPropagation; 95 | var fakeStop = L.DomEvent._fakeStop || stop; 96 | L.DomEvent 97 | .off(content, 'contextmenu', stop) 98 | .off(content, 'click', fakeStop) 99 | .off(content, 'mousedown', stop) 100 | .off(content, 'touchstart', stop) 101 | .off(content, 'dblclick', fakeStop) 102 | .off(content, 'mousewheel', stop) 103 | .off(content, 'wheel', stop) 104 | .off(content, 'scroll', stop) 105 | .off(content, 'MozMousePixelScroll', stop); 106 | 107 | L.DomEvent 108 | .off(container, 'transitionend', 109 | this._handleTransitionEvent, this) 110 | .off(container, 'webkitTransitionEnd', 111 | this._handleTransitionEvent, this); 112 | 113 | if (this._closeButton && this._close) { 114 | var close = this._closeButton; 115 | 116 | L.DomEvent.off(close, 'click', this.hide, this); 117 | } 118 | 119 | return this; 120 | }, 121 | 122 | isVisible: function () { 123 | return L.DomUtil.hasClass(this._container, 'visible'); 124 | }, 125 | 126 | show: function () { 127 | if (!this.isVisible()) { 128 | L.DomUtil.addClass(this._container, 'visible'); 129 | if (this.options.autoPan) { 130 | this._map.panBy([-this.getOffset() / 2, 0], { 131 | duration: 0.5 132 | }); 133 | } 134 | this.fire('show'); 135 | } 136 | }, 137 | 138 | hide: function (e) { 139 | if (this.isVisible()) { 140 | L.DomUtil.removeClass(this._container, 'visible'); 141 | if (this.options.autoPan) { 142 | this._map.panBy([this.getOffset() / 2, 0], { 143 | duration: 0.5 144 | }); 145 | } 146 | this.fire('hide'); 147 | } 148 | if(e) { 149 | L.DomEvent.stopPropagation(e); 150 | } 151 | }, 152 | 153 | toggle: function () { 154 | if (this.isVisible()) { 155 | this.hide(); 156 | } else { 157 | this.show(); 158 | } 159 | }, 160 | 161 | getContainer: function () { 162 | return this._contentContainer; 163 | }, 164 | 165 | getCloseButton: function () { 166 | return this._closeButton; 167 | }, 168 | 169 | setContent: function (content) { 170 | var container = this.getContainer(); 171 | 172 | if (typeof content === 'string') { 173 | container.innerHTML = content; 174 | } else { 175 | // clean current content 176 | while (container.firstChild) { 177 | container.removeChild(container.firstChild); 178 | } 179 | 180 | container.appendChild(content); 181 | } 182 | 183 | return this; 184 | }, 185 | 186 | getOffset: function () { 187 | if (this.options.position === 'right') { 188 | return -this._container.offsetWidth; 189 | } else { 190 | return this._container.offsetWidth; 191 | } 192 | }, 193 | 194 | _handleTransitionEvent: function (e) { 195 | if (e.propertyName == 'left' || e.propertyName == 'right') 196 | this.fire(this.isVisible() ? 'shown' : 'hidden'); 197 | } 198 | }); 199 | 200 | L.control.sidebar = function (placeholder, options) { 201 | return new L.Control.Sidebar(placeholder, options); 202 | }; 203 | -------------------------------------------------------------------------------- /client/static/lib/L.Grid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Grid displays a grid of lat/lng lines on the map. 3 | */ 4 | 5 | L.Grid = L.LayerGroup.extend({ 6 | options: { 7 | xticks: 8, 8 | yticks: 5, 9 | 10 | // 'decimal' or one of the templates below 11 | coordStyle: 'MinDec', 12 | coordTemplates: { 13 | 'MinDec': '{degAbs}° {minDec}\'{dir}', 14 | 'DMS': '{degAbs}{dir}{min}\'{sec}"' 15 | }, 16 | 17 | // Path style for the grid lines 18 | lineStyle: { 19 | stroke: true, 20 | color: '#111', 21 | opacity: 0.6, 22 | weight: 1 23 | }, 24 | 25 | // Redraw on move or moveend 26 | redraw: 'move' 27 | }, 28 | 29 | initialize: function (options) { 30 | L.LayerGroup.prototype.initialize.call(this); 31 | L.Util.setOptions(this, options); 32 | 33 | }, 34 | 35 | onAdd: function (map) { 36 | this._map = map; 37 | 38 | var grid = this.redraw(); 39 | this._map.on('viewreset '+ this.options.redraw, function () { 40 | grid.redraw(); 41 | }); 42 | 43 | this.eachLayer(map.addLayer, map); 44 | }, 45 | 46 | onRemove: function (map) { 47 | // remove layer listeners and elements 48 | map.off('viewreset '+ this.options.redraw, this.map); 49 | this.eachLayer(this.removeLayer, this); 50 | }, 51 | 52 | redraw: function () { 53 | // pad the bounds to make sure we draw the lines a little longer 54 | this._bounds = this._map.getBounds().pad(0.5); 55 | 56 | var grid = []; 57 | var i; 58 | 59 | var latLines = this._latLines(); 60 | for (i in latLines) { 61 | if (Math.abs(latLines[i]) > 90) { 62 | continue; 63 | } 64 | grid.push(this._horizontalLine(latLines[i])); 65 | //grid.push(this._label('lat', latLines[i])); 66 | } 67 | 68 | var lngLines = this._lngLines(); 69 | for (i in lngLines) { 70 | grid.push(this._verticalLine(lngLines[i])); 71 | //grid.push(this._label('lng', lngLines[i])); 72 | } 73 | 74 | this.eachLayer(this.removeLayer, this); 75 | 76 | for (i in grid) { 77 | this.addLayer(grid[i]); 78 | } 79 | return this; 80 | }, 81 | 82 | _latLines: function () { 83 | return this._lines( 84 | this._bounds.getSouth(), 85 | this._bounds.getNorth(), 86 | this.options.yticks * 2, 87 | this._containsEquator() 88 | ); 89 | }, 90 | _lngLines: function () { 91 | return this._lines( 92 | this._bounds.getWest(), 93 | this._bounds.getEast(), 94 | this.options.xticks * 2, 95 | this._containsIRM() 96 | ); 97 | }, 98 | 99 | _lines: function (low, high, ticks, containsZero) { 100 | var delta = low - high; 101 | var tick = delta / ticks; 102 | /* tick = this._round(delta / ticks, delta); 103 | 104 | if (containsZero) { 105 | low = Math.floor(low / tick) * tick; 106 | } else { 107 | low = this._snap(low, tick); 108 | }*/ 109 | var oo = low % tick; 110 | low -= oo; 111 | 112 | var lines = []; 113 | for (var i = -1; i <= ticks; i++) { 114 | lines.push(low - (i * tick)); 115 | } 116 | return lines; 117 | }, 118 | 119 | _containsEquator: function () { 120 | var bounds = this._map.getBounds(); 121 | return bounds.getSouth() < 0 && bounds.getNorth() > 0; 122 | }, 123 | 124 | _containsIRM: function () { 125 | var bounds = this._map.getBounds(); 126 | return bounds.getWest() < 0 && bounds.getEast() > 0; 127 | }, 128 | 129 | _verticalLine: function (lng) { 130 | return new L.Polyline([ 131 | [this._bounds.getNorth(), lng], 132 | [this._bounds.getSouth(), lng] 133 | ], this.options.lineStyle); 134 | }, 135 | _horizontalLine: function (lat) { 136 | return new L.Polyline([ 137 | [lat, this._bounds.getWest()], 138 | [lat, this._bounds.getEast()] 139 | ], this.options.lineStyle); 140 | }, 141 | 142 | _snap: function (num, gridSize) { 143 | return Math.floor(num / gridSize) * gridSize; 144 | }, 145 | 146 | _round: function (num, delta) { 147 | var ret; 148 | 149 | delta = Math.abs(delta); 150 | if (delta >= 1) { 151 | if (Math.abs(num) > 1) { 152 | ret = Math.round(num); 153 | } else { 154 | ret = (num < 0) ? Math.floor(num) : Math.ceil(num); 155 | } 156 | } else { 157 | var dms = this._dec2dms(delta); 158 | if (dms.min >= 1) { 159 | ret = Math.ceil(dms.min) * 60; 160 | } else { 161 | ret = Math.ceil(dms.minDec * 60); 162 | } 163 | } 164 | 165 | return ret; 166 | }, 167 | 168 | _label: function (axis, num) { 169 | var latlng; 170 | var bounds = this._map.getBounds().pad(-0.005); 171 | 172 | if (axis == 'lng') { 173 | latlng = L.latLng(bounds.getNorth(), num); 174 | } else { 175 | latlng = L.latLng(num, bounds.getWest()); 176 | } 177 | 178 | return L.marker(latlng, { 179 | icon: L.divIcon({ 180 | iconSize: [0, 0], 181 | className: 'leaflet-grid-label', 182 | html: '