├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE.md ├── README.md ├── api ├── .flaskenv ├── cesium_entity.py ├── communication.py ├── parsers │ ├── __init__.py │ ├── arduparser.py │ ├── csvparser.py │ ├── djiparser.py │ ├── parser.py │ ├── tlogparser.py │ └── ulgparser.py ├── requirements.txt ├── server.py └── store.py ├── docs ├── demo.png └── tilak.svg ├── obj └── main.gltf ├── package.json ├── package_python.js ├── public ├── electron.js ├── favicon.ico ├── index.html └── preload.js ├── src ├── App.js ├── components │ ├── EntityConfig.js │ ├── Graph.js │ ├── GraphOptions.js │ ├── GraphXY.js │ ├── Heatmap.js │ ├── ToolBar.js │ └── View3D.js ├── controllers │ ├── Entity.js │ └── PlotData.js ├── index.js ├── reportWebVitals.js ├── setupTests.js ├── static │ ├── css │ │ ├── cesium.css │ │ ├── layout.css │ │ ├── loader.css │ │ ├── overlay.css │ │ ├── settings.css │ │ └── test.css │ ├── img │ │ ├── logo.png │ │ └── logoPilou.svg │ ├── js │ │ └── constants.js │ └── vectors │ │ ├── front_view.svg │ │ ├── left_view.svg │ │ └── top_view.svg └── views │ ├── Entities.js │ ├── Loader.js │ ├── Settings.js │ ├── SyncTimestamp.js │ ├── Test.js │ └── layouts │ ├── DetachedLayout.js │ ├── MainLayout.js │ └── SplitLayout.js ├── templates ├── logged_messages.ipynb └── photo_geolocations.ipynb └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Building Executables ⚡ 2 | run-name: ${{ github.actor }} is building TiPlot 🚀 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - "package.json" 10 | 11 | jobs: 12 | Build-AppImage: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - name: Checkout code 📥 16 | uses: actions/checkout@v3 17 | - name: Setup Python 🐍 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | - name: List files 📄 22 | run: tree 🌳 23 | - name: Set variables 📝 24 | run: | 25 | VER=$(grep -E -o '"version": ".*"' package.json | sed -e 's/"version": "//g' | tr -d '"') 26 | echo "VERSION=$VER" >> $GITHUB_ENV 27 | - name: Create Release 🏗️ 28 | id: create_release 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: v${{ env.VERSION }} 34 | release_name: Tiplot v${{ env.VERSION }} 35 | body: ${{ github.event.head_commit.message }} 36 | draft: true 37 | prerelease: false 38 | - name: Restore virtualenv cache 📦 39 | uses: syphar/restore-virtualenv@v1 40 | id: cache-virtualenv 41 | with: 42 | requirement_files: api/requirements.txt 43 | - name: Restore pip download cache 📦 44 | uses: syphar/restore-pip-download-cache@v1 45 | if: steps.cache-virtualenv.outputs.cache-hit != 'true' 46 | - name: Install Python dependencies 🐍 47 | run: pip install -r api/requirements.txt 48 | if: steps.cache-virtualenv.outputs.cache-hit != 'true' 49 | - name: Install Node dependencies 📦 50 | run: yarn install 51 | - name: Build backend 🔨 52 | run: yarn build:api 53 | - name: Build desktop app 💻 54 | run: yarn build:electron 55 | env: 56 | GH_TOKEN: ${{ github.token }} 57 | # CI: false # ignore warnings 58 | 59 | Build-EXE: 60 | runs-on: windows-latest 61 | steps: 62 | - name: Checkout code 📥 63 | uses: actions/checkout@v3 64 | - name: Setup Python 🐍 65 | uses: actions/setup-python@v4 66 | with: 67 | python-version: "3.10" 68 | - name: List files 📄 69 | run: tree 🌳 70 | - name: Install Node dependencies 📦 71 | run: yarn install 72 | - name: Install Python dependencies 🐍 73 | run: pip install -r api/requirements.txt 74 | - name: Build backend 🔨 75 | run: yarn build:api 76 | - name: Build desktop app 💻 77 | run: yarn build:electron 78 | env: 79 | GH_TOKEN: ${{ github.token }} 80 | # CI: false # ignore warnings 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /api/venv 6 | /api/__pycache__/ 7 | /api/parsers/__pycache__/ 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | /api.spec 17 | /dist 18 | /logs/* 19 | /backend/* 20 | *.spec 21 | 22 | # misc 23 | .DS_Store 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | # yarn.lock 33 | package-lock.json 34 | 35 | # old files 36 | *.old 37 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | 5 | build: 6 | stage: build 7 | image: registry.gitlab.com/tilak.io/tiplotci 8 | before_script: 9 | - mv /docker_tiplot/* . 10 | - CI= yarn install 11 | - CI= pip install -r api/requirement.txt 12 | script: 13 | - CI= yarn build:api 14 | - CI= yarn build:electron 15 | artifacts: 16 | paths: 17 | - dist/*.AppImage 18 | only: 19 | - main 20 | - tag 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![https://tilak.io/](docs/tilak.svg) 3 | 4 | # About Tilak.io 5 | 6 | We are an engineering services company that focus on drone technologies. 7 | We decided to opensource TiPlot, our log visualising tool, so the world can benefit from a nice and easy way to display logs from **PX4**, **CSV**, or even from your **Python** code or your **Jupyter Notebook**. 8 | 9 | # Feature Request 10 | 11 | Please reach out to us via our website [Tilak.io](https://tilak.io/), we are happy to help ! 12 | 13 | # About TiPlot 14 | 15 | TiPlot is an open-source visualisation tool for flight logs. With TiPlot, you can easily visualize your flight data in 2D graphs and a 3D view, helping you better understand the performance and behavior of your unmanned aerial vehicle (UAV). 16 | 17 | ![Entities](docs/demo.png) 18 | 19 | ## Features 20 | 21 | - Supports multiple file formats, including : 22 | - PX4's .ulg 23 | - Generic .csv 24 | - DJI's .DAT 25 | - QGroundControl's .tlog 26 | - Ardupilot's .BIN Logs. 27 | - 2D yt and xy graphs for visualizing flight data. 28 | - 3D viewer for displaying the paths of your UAVs. 29 | - Add multiple UAVs to compare flights. 30 | - Sync the 3D view with a specific timestamp by hovering over it. 31 | - Perform customized operations on data currently being processed. 32 | - Ability to send a data dictionary and entities to be displayed in the 3D view over TCP port `5555` from a Python script or a Jupyter notebook. 33 | - Layouts are automatically saved after every change. 34 | - Drag and drop plots to reposition them. 35 | - Plot multiple fields from different topics in yt graphs. 36 | - X scale of all graphs is synced when zooming in. 37 | - Y scale of graphs is automatically set to fit all available data in the timestamp range. 38 | - Change view layout from split to detached to detach the 3D view as a new window. 39 | - Adjust camera settings and change background color and show grids on the 3 axis in the settings page. 40 | 41 | ## Installation 42 | 43 | ### Prebuilt AppImage 44 | 45 | To run TiPlot using the prebuilt AppImage, follow these steps: 46 | 47 | 1. Download the latest AppImage from the [releases page](https://github.com/tilak-io/tiplot/releases/latest). 48 | 2. Make the AppImage executable by running: 49 | ```bash 50 | cd ~/Downloads 51 | chmod +x tiplot-1.x.x.AppImage 52 | ``` 53 | 3. Run the AppImage: 54 | ```bash 55 | ./tiplot-1.x.x.AppImage 56 | ``` 57 | 58 | ### Building from source 59 | 60 | To build TiPlot from source, follow these steps: 61 | 62 | 1. Clone the repository: 63 | ``` 64 | git clone https://github.com/tilak-io/tiplot 65 | cd tiplot 66 | ``` 67 | 2. Install dependencies: 68 | ``` 69 | # Install Node dependencies 70 | yarn install 71 | # Install Python dependencies: 72 | pip3 install -r api/requirements.txt 73 | ``` 74 | 3. Start the API server: 75 | 76 | ``` 77 | yarn start:api 78 | ``` 79 | In a new terminal, start the front end: 80 | ``` 81 | yarn start 82 | ``` 83 | 84 | ## Usage 85 | To use TiPlot, follow these steps: 86 | 87 | 1. Launch TiPlot's AppImage, exe or compiled build. 88 | 2. Click the "Browse" button to select a flight log file. 89 | 3. Add a graph (yt or xy). 90 | 4. Use the ALT + hover feature to sync the 3D view with a specific timestamp. 91 | 5. Use the entities page to choose the UAV's position/attitude tables. 92 | 93 | ## Sending data from a Python script or Jupyter notebook 94 | To send data to TiPlot from a Python script or Jupyter notebook, you can use the following code: 95 | ```python 96 | import zmq 97 | import zlib 98 | import pickle 99 | import pyulog 100 | import pandas as pd 101 | 102 | port = '5555' 103 | context = zmq.Context() 104 | socket = context.socket(zmq.REQ) 105 | socket.connect("tcp://0.0.0.0:%s" % port) 106 | 107 | def send_zipped_pickle(socket, obj, flags=0, protocol=-1): 108 | p = pickle.dumps(obj, protocol) 109 | z = zlib.compress(p) 110 | return socket.send(z, flags=flags) 111 | 112 | send_zipped_pickle(socket, [datadict, []]) 113 | ``` 114 | 115 | Note that `datadict` is the data dictionary returned by the parser. 116 | `[]` is the entities array (empty array means we don't want to display anything on 3d view) 117 | 118 | For more info check out the Jupyter notebooks in the [templates](https://github.com/tilak-io/tiplot/blob/main/templates) folder. 119 | 120 | ## Running Sequences and Custom Operations 121 | 122 | In this guide, we will walk you through the process of creating and executing sequences, which are Python functions containing a set of operations to manipulate log data. These sequences can be customized to suit your specific needs. Follow these steps to get started: 123 | 124 | 1. **Set Preferred Editor**: Ensure that you have configured your preferred code editor in the tool settings by navigating to `Tools > Settings > General > External Editor`. If you intend to use a terminal-based editor such as Vim or Nano, make sure to specify the appropriate command to launch the terminal. (i.e: `kitty nvim` for kitty, `gnome-terminal -- nvim` for ubuntu's default terminal, `xterm -e nvim` for xterm...) 125 | 126 | 2. **Create a New Sequence**: To begin, create a new sequence by navigating to `Tools > Sequences > Add New Sequence`. 127 | 128 | 3. **Provide a Descriptive Name**: Give your sequence a meaningful and descriptive name that reflects its purpose. 129 | 130 | 4. **Edit the Sequence**: Upon creating a new sequence, your preferred code editor will open with a template function. You can now define the operations you want to perform on the log data. 131 | 132 | Here's an example of a sequence that transforms log data from the NED (North-East-Down) representation to the ENU (East-North-Up) representation for a topic called `vehicle_local_position_enu`: 133 | 134 | ```python 135 | def handle_data(datadict): 136 | import numpy as np 137 | new = datadict 138 | # Coordinate transformation matrix from NED to ENU 139 | NED_to_ENU = np.array([ 140 | [0, 1, 0], # East (ENU X) corresponds to North (NED Y) 141 | [1, 0, 0], # North (ENU Y) corresponds to East (NED X) 142 | [0, 0, -1] # Up (ENU Z) corresponds to -Down (NED Z) 143 | ]) 144 | # Apply the coordinate transformation to the 'x', 'y', and 'z' columns 145 | new['vehicle_local_position_enu'] = new['vehicle_local_position'].copy() 146 | xyz_ned = new['vehicle_local_position_enu'][['x', 'y', 'z']].values 147 | xyz_enu = np.dot(xyz_ned, NED_to_ENU.T) # Transpose the transformation matrix for NED to ENU 148 | new['vehicle_local_position_enu'][['x', 'y', 'z']] = xyz_enu 149 | return new 150 | ``` 151 | 152 | ***NOTE: the imports should be inside the function*** 153 | 154 | Feel free to adapt and customize your sequence function to perform the operations that are relevant to your data processing needs. 155 | 156 | Happy sequencing 🎉! 157 | 158 | ## Contributing 159 | We welcome contributions to TiPlot. If you would like to contribute, please follow these steps: 160 | 161 | 1. Fork the repository. 162 | 2. Create a new branch for your changes. 163 | 3. Make your changes and commit them to your branch. 164 | 4. Push your branch to your forked repository. 165 | 5. Create a new pull request. 166 | 167 | ## License 168 | This project is licensed under the Apache License, Version 2.0. See the [LICENSE](https://github.com/tilak-io/tiplot/blob/main/LICENSE.md) file for details. 169 | -------------------------------------------------------------------------------- /api/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=main.py 2 | FLASK_DEBUG=false 3 | -------------------------------------------------------------------------------- /api/cesium_entity.py: -------------------------------------------------------------------------------- 1 | class CesiumEntity: 2 | next_id = 0 3 | def __init__(self, 4 | name, 5 | position, 6 | attitude, 7 | color="white", 8 | wireframe=False, 9 | tracked=True, 10 | active=True, 11 | pathColor="blue", 12 | alpha=1, 13 | useRPY=False, 14 | useXYZ=True, 15 | scale=1, 16 | viewModel=None): 17 | 18 | self.name = name 19 | self.color = color 20 | self.wireframe = wireframe 21 | self.pathColor = pathColor 22 | self.position = position 23 | self.attitude = attitude 24 | self.alpha = alpha 25 | self.useRPY = useRPY 26 | self.useXYZ = useXYZ 27 | self.tracked = tracked 28 | self.active = active 29 | self.scale = scale 30 | self.viewModel = viewModel 31 | self.id = CesiumEntity.next_id 32 | CesiumEntity.next_id += 1 33 | 34 | 35 | @classmethod 36 | def fromJson(cls, json): 37 | name = json['name'] 38 | position = json['position'] 39 | attitude = json['attitude'] 40 | if "alpha" in json: 41 | alpha = json['alpha'] 42 | else: 43 | alpha = 1 44 | 45 | if "useRPY" in json: 46 | useRPY = json['useRPY'] 47 | else: 48 | useRPY = False 49 | 50 | if "useXYZ" in json: 51 | useXYZ = json['useXYZ'] 52 | else: 53 | useXYZ = True 54 | 55 | if "viewModel" in json: 56 | viewModel = json['viewModel'] 57 | else: 58 | viewModel = None 59 | 60 | if "pathColor" in json: 61 | pathColor = json['pathColor'] 62 | else: 63 | pathColor = "blue" 64 | 65 | if "color" in json: 66 | color = json['color'] 67 | else: 68 | color = "white" 69 | 70 | 71 | if "wireframe" in json: 72 | wireframe = json['wireframe'] 73 | else: 74 | wireframe = False 75 | 76 | if "tracked" in json: 77 | tracked = json['tracked'] 78 | else: 79 | tracked = True 80 | 81 | if "active" in json: 82 | active = json['active'] 83 | else: 84 | active = True 85 | 86 | if "scale" in json: 87 | scale = json['scale'] 88 | else: 89 | scale = 1 90 | 91 | return cls( 92 | name=name, 93 | color=color, 94 | wireframe=wireframe, 95 | pathColor=pathColor, 96 | position=position, 97 | attitude=attitude, 98 | alpha=alpha, 99 | useRPY=useRPY, 100 | useXYZ=useXYZ, 101 | tracked=tracked, 102 | active=active, 103 | scale=scale, 104 | viewModel=viewModel) 105 | 106 | def toJson(self): 107 | return({"id": self.id, 108 | "name": self.name, 109 | "color": self.color, 110 | "wireframe": self.wireframe, 111 | "pathColor": self.pathColor, 112 | "alpha": self.alpha, 113 | "useXYZ": self.useXYZ, 114 | "useRPY": self.useRPY, 115 | "tracked": self.tracked, 116 | "active": self.active, 117 | "scale": self.scale, 118 | "position": self.position, 119 | "attitude": self.attitude 120 | }) 121 | -------------------------------------------------------------------------------- /api/communication.py: -------------------------------------------------------------------------------- 1 | import zmq, zlib, time, pickle, store 2 | from cesium_entity import CesiumEntity 3 | from threading import Thread 4 | 5 | class Comm(Thread): 6 | def __init__(self, io=None, port=5555): 7 | self.delay = 0.5 8 | super(Comm, self).__init__() 9 | self.port = port 10 | self.context = zmq.Context() 11 | self.socket = self.context.socket(zmq.REP) 12 | self.io = io 13 | try: 14 | self.socket.bind("tcp://*:%s" % port) 15 | print('-> binded on %s' % (self.socket.LAST_ENDPOINT).decode('utf-8')) 16 | except: 17 | print('~> addr in use') 18 | 19 | 20 | def send_zipped_pickle(self, obj, flags=0, protocol=-1): 21 | p = pickle.dumps(obj, protocol) 22 | z = zlib.compress(p) 23 | return self.socket.send(z, flags=flags) 24 | 25 | def recv_zipped_pickle(self, flags=0, protocol=-1): 26 | # print('-> waiting for data') 27 | z = self.socket.recv(flags) 28 | p = zlib.decompress(z) 29 | return pickle.loads(p) 30 | 31 | def map_entities(self, entities): 32 | mapped = [] 33 | for entity in entities: 34 | mapped.append(CesiumEntity.fromJson(entity)) 35 | return mapped 36 | 37 | 38 | def listen_for_data(self): 39 | while True: 40 | try: 41 | [datadict, json_entities] = self.recv_zipped_pickle(zmq.NOBLOCK) 42 | entities = self.map_entities(json_entities) 43 | print('-> data recieved...') 44 | self.io.emit('entities_loaded') 45 | store.Store.get().setStore(datadict, entities) 46 | self.send_zipped_pickle('hi') 47 | except zmq.Again as e: 48 | pass 49 | time.sleep(self.delay) 50 | 51 | def run(self): 52 | self.listen_for_data() 53 | -------------------------------------------------------------------------------- /api/parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilak-io/tiplot/9dbacac7484fe33914e57ea05caa38472dcc8dd7/api/parsers/__init__.py -------------------------------------------------------------------------------- /api/parsers/arduparser.py: -------------------------------------------------------------------------------- 1 | from pandas import DataFrame 2 | from cesium_entity import CesiumEntity 3 | from .parser import Parser 4 | from pymavlink import mavutil 5 | 6 | class ArduParser(Parser): 7 | def __init__(self): 8 | super().__init__() 9 | self.name = "ardu_parser" 10 | self.initDefaultEntity() 11 | self.initEntities() 12 | 13 | def parse(self,filename): 14 | mlog = mavutil.mavlink_connection(filename) 15 | buf = {} 16 | 17 | while mlog.remaining: 18 | m = mlog.recv_match() 19 | if (m is None): break 20 | name = m.get_type() 21 | data = m.to_dict() 22 | if 'TimeUS' in list(data.keys()): 23 | data['timestamp_tiplot'] = data['TimeUS'] / 1e6 24 | else: 25 | #ignore tables with no timestamp 26 | continue 27 | 28 | if(name in buf): 29 | del data['mavpackettype'] 30 | buf[name].append(data) 31 | else: 32 | clean_dict = m.to_dict() 33 | del clean_dict['mavpackettype'] 34 | buf[name] = [clean_dict] 35 | 36 | self.datadict = {i:DataFrame(buf[i]).bfill() for i in buf} 37 | return self.datadict, self.entities 38 | 39 | 40 | def initDefaultEntity(self): 41 | self.default_entity = CesiumEntity(name='ardu pilot default entity', 42 | useRPY=False, 43 | useXYZ=False, 44 | color="#ffffff", 45 | pathColor="#0000ff", 46 | position={ 47 | 'table':'AHR2', 48 | 'longitude': 'Lng', 49 | 'lattitude': 'Lat', 50 | 'altitude': 'Alt', 51 | }, 52 | attitude={ 53 | 'table':'AHR2', 54 | 'q0':'Q1', 55 | 'q1':'Q2', 56 | 'q2':'Q3', 57 | 'q3':'Q4', 58 | }) 59 | 60 | def setDefaultEntities(self): 61 | self.addEntity(self.default_entity) 62 | -------------------------------------------------------------------------------- /api/parsers/csvparser.py: -------------------------------------------------------------------------------- 1 | from pandas import read_csv, to_datetime 2 | import numpy as np 3 | from cesium_entity import CesiumEntity 4 | from .parser import Parser 5 | import sys 6 | import importlib 7 | import os 8 | 9 | class CSVParser(Parser): 10 | def __init__(self): 11 | super().__init__() 12 | self.name = "csv_parser" 13 | self.initDefaultEntitiy() 14 | self.initEntities() 15 | 16 | def parse(self,filename): 17 | csv = read_csv(filename, low_memory=False) 18 | start_time = to_datetime(csv['timestamp'][0]) 19 | time_delta = (to_datetime(csv['timestamp']) - start_time) 20 | seconds = time_delta / np.timedelta64(1, 's') 21 | micro_seconds = time_delta / np.timedelta64(1, 'us') 22 | csv['timestamp'] = micro_seconds 23 | csv['timestamp_tiplot'] = seconds 24 | self.datadict = {"data": csv} 25 | return self.datadict, self.entities 26 | 27 | def initDefaultEntitiy(self): 28 | self.default_entity = CesiumEntity( 29 | color="#ffffff", 30 | pathColor="#0000ff", 31 | name='csv default entity', 32 | alpha=1, 33 | useRPY=True, 34 | useXYZ=False, 35 | position={ 36 | 'table':'data', 37 | 'longitude':'lon', 38 | 'lattitude':'lat', 39 | 'altitude':'altitude', 40 | }, 41 | attitude={ 42 | 'table':'data', 43 | 'roll':'roll', 44 | 'pitch':'pitch', 45 | 'yaw':'yaw', 46 | }) 47 | 48 | 49 | def setDefaultEntities(self): 50 | self.addEntity(self.default_entity) 51 | -------------------------------------------------------------------------------- /api/parsers/djiparser.py: -------------------------------------------------------------------------------- 1 | import math 2 | from pandas import DataFrame 3 | import struct 4 | from .parser import Parser 5 | from cesium_entity import CesiumEntity 6 | 7 | class DJIParser(Parser): 8 | def __init__(self): 9 | super().__init__() 10 | self.name = "dji_parser" 11 | self.initDefaultEntitiy() 12 | self.initEntities() 13 | 14 | def quaternionToEuler(self,q): 15 | 16 | #roll (x-axis rotation) 17 | sinr_cosp = 2 * (q['quatW'] * q['quatX'] + q['quatY'] * q['quatZ']) 18 | cosr_cosp = 1 - 2 * (q['quatX'] * q['quatX'] + q['quatY'] * q['quatY']) 19 | q['roll'] = math.atan2(sinr_cosp, cosr_cosp) 20 | 21 | #pitch (y-axis rotation) 22 | sinp = 2 * (q['quatW'] * q['quatY'] - q['quatZ'] * q['quatX']) 23 | if (abs(sinp) >= 1): 24 | q['pitch'] = math.copysign(math.pi/ 2, sinp) 25 | else: 26 | q['pitch']= math.asin(sinp) 27 | 28 | #yaw (z-axis rotation) 29 | siny_cosp = 2 * (q['quatW'] * q['quatZ'] + q['quatX'] * q['quatY']) 30 | cosy_cosp = 1 - 2 * (q['quatY'] * q['quatY'] + q['quatZ'] * q['quatZ']) 31 | q['yaw'] = math.atan2(siny_cosp, cosy_cosp) 32 | 33 | return q 34 | 35 | def parse(self,filename): 36 | f = open(filename,'rb') 37 | buffer = f.read() 38 | packets = [] 39 | for index in range(128,len(buffer)): 40 | if(index<(len(buffer)-2) and buffer[index]==0x55 and buffer[index+2]==0x00): ## New Packet 41 | header = struct.unpack('-math.pi and data['latitude']0.0175 and abs(data['latitude'])>0.0175): 63 | ## lon/lat in degrees 64 | data['longitude']=math.degrees(data['longitude']) 65 | data['latitude']=math.degrees(data['latitude']) 66 | data['time']=p['tickNb'] 67 | data = self.quaternionToEuler(data) 68 | gps_data.append(data) 69 | 70 | out = DataFrame(gps_data).bfill() 71 | out['timestamp_tiplot'] = (out['time'] - out['time'][0]) / 1e6 72 | out = out.fillna(0) 73 | self.datadict = {"data": out} 74 | return self.datadict, self.entities 75 | 76 | def initDefaultEntitiy(self): 77 | self.default_entity = CesiumEntity( 78 | name='dji dat default entity', 79 | useRPY=True, 80 | useXYZ=False, 81 | color="#ffffff", 82 | pathColor="#0000ff", 83 | position={ 84 | 'table':'data', 85 | 'longitude':'longitude', 86 | 'lattitude':'latitude', 87 | 'altitude':'altitude', 88 | }, 89 | attitude={ 90 | 'table':'data', 91 | 'roll':'roll', 92 | 'pitch':'pitch', 93 | 'yaw':'yaw', 94 | }) 95 | 96 | def setDefaultEntities(self): 97 | self.addEntity(self.default_entity) 98 | -------------------------------------------------------------------------------- /api/parsers/parser.py: -------------------------------------------------------------------------------- 1 | from cesium_entity import CesiumEntity 2 | from os import path, makedirs 3 | import json 4 | 5 | class Parser: 6 | def __init__(self): 7 | self.name = "generic_parser" 8 | self.entities = [] 9 | self.datadict = {} 10 | self.additionalInfo = [] 11 | 12 | 13 | def parse(self,filename): 14 | print("Parsing file:" + filename) 15 | 16 | def addEntity(self,entity): 17 | self.entities.append(entity) 18 | 19 | def setLayout(self, layout): 20 | self.layout = layout 21 | 22 | def setDefaultEntities(self): pass 23 | 24 | def initEntities(self): 25 | config_folder = path.expanduser("~/Documents/tiplot/config/") 26 | if not path.exists(config_folder): 27 | makedirs(config_folder) 28 | config_file = config_folder + self.name + ".json" 29 | if (path.exists(config_file)): 30 | # print("+ " + self.name + " config found") 31 | # print("+ " + config_file) 32 | file = open(config_file) 33 | entities = json.load(file) 34 | for entity in entities: 35 | mapped_entity = CesiumEntity.fromJson(entity) 36 | self.addEntity(mapped_entity) 37 | else: 38 | # print("- " + self.name + " config not found") 39 | # print("- " + config_file) 40 | # print("- Using default config for: " + self.name) 41 | self.setDefaultEntities() 42 | -------------------------------------------------------------------------------- /api/parsers/tlogparser.py: -------------------------------------------------------------------------------- 1 | from pandas import DataFrame 2 | from cesium_entity import CesiumEntity 3 | from .parser import Parser 4 | from pymavlink import mavutil 5 | import datetime 6 | 7 | class TLOGParser(Parser): 8 | def __init__(self): 9 | super().__init__() 10 | self.name = "tlog_parser" 11 | self.initDefaultEntity() 12 | self.initEntities() 13 | 14 | def parse(self,filename): 15 | mlog = mavutil.mavlink_connection(filename) 16 | buf = {} 17 | 18 | while True: 19 | m = mlog.recv_match() 20 | if (m is None): break 21 | data = m.to_dict() 22 | name = data['mavpackettype'] 23 | data['timestamp_tiplot'] = m._timestamp 24 | data['time_utc_usec'] = m._timestamp*1e6 25 | try: 26 | data['time_utc'] = str(datetime.datetime.fromtimestamp(m._timestamp)) 27 | except: 28 | pass 29 | 30 | if(name in buf): 31 | del data['mavpackettype'] 32 | buf[name].append(data) 33 | else: 34 | clean_dict = m.to_dict() 35 | del clean_dict['mavpackettype'] 36 | buf[name] = [clean_dict] 37 | 38 | self.datadict = {i:DataFrame(buf[i]).bfill() for i in buf} 39 | return self.datadict, self.entities 40 | 41 | 42 | def initDefaultEntity(self): 43 | self.default_entity = CesiumEntity(name='tlog default entity', 44 | useRPY=True, 45 | useXYZ=True, 46 | color="#ffffff", 47 | pathColor="#0000ff", 48 | position={ 49 | 'table':'LOCAL_POSITION_NED', 50 | 'x': 'x', 51 | 'y': 'y', 52 | 'z': 'z', 53 | }, 54 | attitude={ 55 | 'table':'ATTITUDE', 56 | 'roll':'roll', 57 | 'pitch':'pitch', 58 | 'yaw':'yaw', 59 | }) 60 | 61 | def setDefaultEntities(self): 62 | self.addEntity(self.default_entity) 63 | -------------------------------------------------------------------------------- /api/parsers/ulgparser.py: -------------------------------------------------------------------------------- 1 | from pyulog import ULog 2 | from pandas import DataFrame 3 | from cesium_entity import CesiumEntity 4 | import math 5 | import numpy as np 6 | from .parser import Parser 7 | 8 | class ULGParser(Parser): 9 | def __init__(self): 10 | super().__init__() 11 | self.name = "ulg_parser" 12 | self.ulg = None 13 | self.initDefaultEntity() 14 | self.initEntities() 15 | 16 | def euler_from_quaternion(self, w, x, y, z): 17 | norm = math.sqrt(w*w + x*x + y*y + z*z) 18 | w, x, y, z = w / norm, x / norm, y / norm, z / norm 19 | 20 | angles = {} 21 | #roll(x-axis rotation) 22 | sinr_cosp = 2 * (w * x + y * z) 23 | cosr_cosp = 1 - 2 * (x * x + y * y) 24 | angles['roll'] = math.atan2(sinr_cosp, cosr_cosp) 25 | 26 | #pitch (y-axis rotation) 27 | sinp = math.sqrt(1+2*(w*y - x*z)) 28 | cosp = math.sqrt(1-2*(w*y - x*z)) 29 | angles['pitch'] = 2*math.atan2(sinp,cosp) - math.pi/2 30 | 31 | # yaw (z-axis rotation) 32 | siny_cosp = 2 * (w * z + x * y) 33 | cosy_cosp = 1 - 2 * (y * y + z * z) 34 | angles['yaw'] = math.atan2(siny_cosp, cosy_cosp) 35 | return angles 36 | 37 | 38 | 39 | def add_euler(self,datadict): 40 | if "vehicle_attitude" in datadict: 41 | a=datadict['vehicle_attitude'] 42 | result = [] 43 | if('q[0]' in a and \ 44 | 'q[1]' in a and \ 45 | 'q[2]' in a and \ 46 | 'q[3]' in a): 47 | for i in a.to_dict('records'): 48 | result.append(self.euler_from_quaternion( 49 | i['q[0]'], 50 | i['q[1]'], 51 | i['q[2]'], 52 | i['q[3]'],)) 53 | r = DataFrame(result) 54 | a['pitch'] = r['pitch'] 55 | a['roll'] = r['roll'] 56 | a['yaw'] = r['yaw'] 57 | a['pitch_deg'] = r['pitch']*180.0/np.pi 58 | a['roll_deg'] = r['roll']*180.0/np.pi 59 | a['yaw_deg'] = r['yaw']*180.0/np.pi 60 | 61 | def parse(self,filename): 62 | self.ulg = ULog(filename) 63 | self.datadict = {} 64 | for data in self.ulg.data_list: 65 | if data.multi_id > 0: 66 | name = f"{data.name}_{data.multi_id}" 67 | else: 68 | name = data.name 69 | self.datadict[name] = DataFrame(data.data) 70 | self.datadict[name]['timestamp_tiplot'] = self.datadict[name]['timestamp'] / 1e6 71 | self.add_euler(self.datadict) 72 | self.setAdditionalInfo() 73 | return self.datadict, self.entities, self.additionalInfo 74 | 75 | def setAdditionalInfo(self): 76 | message_data = [] 77 | for message in self.ulg.logged_messages: 78 | message_dict = {} 79 | message_dict['timestamp'] = message.timestamp 80 | message_dict['message'] = message.message 81 | # message_dict['log_level'] = message.log_level 82 | # message_dict['log_level_str'] = message.log_level_str 83 | message_dict['timestamp_tiplot'] = message_dict['timestamp'] / 1e6 84 | message_data.append(message_dict) 85 | self.datadict['logged_messages'] = {} 86 | messages_df = DataFrame(message_data) 87 | 88 | # exclude 'timestamp_tiplot' from infobox 89 | columns_to_include = [col for col in messages_df.columns if col != 'timestamp_tiplot'] 90 | filtered_df = messages_df[columns_to_include] 91 | self.datadict['logged_messages'] = messages_df 92 | self.additionalInfo.append({"name": "Logged Messages", "info": filtered_df.to_dict('records'), "search": False}) 93 | 94 | 95 | parameters_dict = [{"index": idx, "name": name, "value": value} for idx, (name, value) in enumerate(self.ulg.initial_parameters.items())] 96 | self.additionalInfo.append({"name": "Initial Parameters", "info": parameters_dict, "search": True}) 97 | 98 | version_info = [{"name": key, "value": value} for key, value in self.ulg.msg_info_dict.items()] 99 | self.additionalInfo.append({"name": "Version", "info": version_info, "search": False}) 100 | 101 | def initDefaultEntity(self): 102 | self.default_entity = CesiumEntity(name='ulg default entity', 103 | color="#ffffff", 104 | pathColor="#0000ff", 105 | useRPY=False, 106 | useXYZ=True, 107 | position={ 108 | 'table':'vehicle_local_position', 109 | 'x':'x', 110 | 'y':'y', 111 | 'z':'z', 112 | }, 113 | attitude={ 114 | 'table':'vehicle_attitude', 115 | 'q0':'q[0]', 116 | 'q1':'q[1]', 117 | 'q2':'q[2]', 118 | 'q3':'q[3]', 119 | }) 120 | 121 | def setDefaultEntities(self): 122 | self.addEntity(self.default_entity) 123 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | PyInstaller 2 | pandas 3 | pyulog 4 | zmq 5 | flask 6 | flask_cors 7 | flask_socketio 8 | python-dotenv 9 | python-engineio 10 | gevent-websocket 11 | pymavlink 12 | -------------------------------------------------------------------------------- /api/server.py: -------------------------------------------------------------------------------- 1 | from engineio.async_drivers import gevent 2 | from flask import Flask, request, send_file 3 | from flask_socketio import SocketIO, emit 4 | from flask_cors import CORS 5 | from threading import Thread 6 | from time import localtime, strftime 7 | from os import makedirs, path, getcwd, environ 8 | from glob import glob 9 | from communication import Comm 10 | from datetime import datetime 11 | from argparse import ArgumentParser 12 | import pandas as pd 13 | import store 14 | import json 15 | import subprocess 16 | import zmq, zlib, pickle 17 | import traceback 18 | 19 | from parsers.ulgparser import ULGParser 20 | from parsers.csvparser import CSVParser 21 | from parsers.djiparser import DJIParser 22 | from parsers.arduparser import ArduParser 23 | from parsers.tlogparser import TLOGParser 24 | 25 | app = Flask(__name__) 26 | app.config['SECRET_KEY'] = 'secret!' 27 | CORS(app,resources={r"/*":{"origins":"*"}}) 28 | socketio = SocketIO(app,cors_allowed_origins="*") 29 | 30 | logs_dir = path.expanduser("~/Documents/tiplot/logs/") 31 | logs_dir = logs_dir.replace("\\", "/") 32 | 33 | configs_dir = path.expanduser("~/Documents/tiplot/config/") 34 | sequences_dir = path.expanduser("~/Documents/tiplot/sequences/") 35 | 36 | if not path.exists(logs_dir): 37 | makedirs(logs_dir) 38 | 39 | if not path.exists(sequences_dir): 40 | makedirs(sequences_dir) 41 | 42 | thread = Thread() 43 | current_parser = None 44 | current_file = None 45 | current_ext = None 46 | 47 | extension_to_parser = { 48 | 'ulg': ULGParser, 49 | 'csv': CSVParser, 50 | 'dat': DJIParser, 51 | 'bin': ArduParser, 52 | 'tlog': TLOGParser, 53 | } 54 | 55 | def choose_parser(file, logs_dir, isExtra=False): 56 | 57 | global current_parser, current_ext 58 | full_path = logs_dir + file 59 | 60 | _, file_extension = path.splitext(full_path) 61 | file_extension = file_extension.lower()[1:] # remove the leading '.' 62 | current_ext = file_extension 63 | 64 | # Look up the parser class in the dictionary using the file extension as the key 65 | parser_cls = extension_to_parser.get(file_extension) 66 | if parser_cls is None: 67 | return False 68 | # raise ValueError(f"Unsupported file extension: {file_extension}") 69 | 70 | # Create an instance of the parser class and use it to parse the file 71 | parser = parser_cls() 72 | try: 73 | datadict, entities, additional_info = parser.parse(full_path) 74 | if isExtra: 75 | store.Store.get().setExtra(datadict) 76 | else: 77 | store.Store.get().setStore(datadict, entities, additional_info) 78 | ok = True 79 | current_parser = parser 80 | except ValueError: 81 | datadict, entities = parser.parse(full_path) 82 | if isExtra: 83 | store.Store.get().setExtra(datadict) 84 | else: 85 | store.Store.get().setStore(datadict, entities) 86 | ok = True 87 | current_parser = parser 88 | return ok 89 | 90 | @socketio.on("connect") 91 | def connected(): 92 | # print("-> client has connected " + request.sid) 93 | global thread 94 | if not thread.is_alive(): 95 | print("-> Starting Communications Thread...") 96 | thread = Comm(socketio) 97 | thread.daemon = True 98 | thread.start() 99 | 100 | @socketio.on('get_table_columns') 101 | def get_table_columns(data): 102 | index = data['index'] 103 | table = data['table'] 104 | columns = store.Store.get().getTableColumns(table) 105 | data = {"index": index,"table": table, "columns": columns} 106 | emit('table_columns', data) 107 | 108 | @app.route('/upload_log', methods=['POST']) 109 | def upload_log(): 110 | isExtra = request.headers["isExtra"] 111 | if (isExtra == "true"): 112 | isExtra = True 113 | else: 114 | isExtra = False 115 | try: 116 | file = request.files['log'] 117 | if file: 118 | file.save(path.join(logs_dir, file.filename)) 119 | ok = choose_parser(file.filename, logs_dir, isExtra) 120 | except: 121 | ok = False 122 | return {'ok': ok} 123 | 124 | @app.route('/model') 125 | def model_3d(): 126 | model = args.model 127 | return send_file(model) 128 | 129 | @app.route('/entities_config') 130 | def entities_config(): 131 | config = store.Store.get().getEntities() 132 | res = {"config": config} 133 | return res 134 | 135 | @app.route('/default_entity') 136 | def default_entity(): 137 | if current_parser is not None: 138 | default = current_parser.default_entity.toJson() 139 | else: 140 | default = {} 141 | return default 142 | 143 | @app.route('/set_tracked_entity', methods=['POST']) 144 | def set_tracked_entity(): 145 | entity_id = request.get_json('id') 146 | config = store.Store.get().getEntities() 147 | if not config: 148 | return {"ok": False, "msg": "Entity list is empty."} 149 | entity_name = "" 150 | entity_found = False 151 | for entity in config: 152 | if entity['id'] == entity_id: 153 | entity['tracked'] = True 154 | entity_name = entity['name'] 155 | entity_found = True 156 | else: 157 | entity['tracked'] = False 158 | if not entity_found: 159 | return {"ok": False, "msg": f"No entity with ID {entity_id} found."} 160 | store.Store.get().setEntities(config) 161 | return {"ok": True, "msg": f"\"{entity_name}\" is now tracked."} 162 | 163 | @app.route('/write_config', methods=['POST']) 164 | def write_config(): 165 | configs = request.get_json() 166 | if (current_parser is None): 167 | name = "custom_parser" 168 | else: 169 | name = current_parser.name 170 | ok, msg = store.Store.get().validateEntities(configs) 171 | if not ok: 172 | return {"ok": ok, "msg": msg} 173 | store.Store.get().setEntities(configs) 174 | with open(configs_dir + name + ".json", "w") as outfile: 175 | outfile.write(json.dumps(configs, indent=2)) 176 | return {'ok': ok, "msg": msg} 177 | 178 | @app.route('/validate_config', methods=['POST']) 179 | def validate_config(): 180 | configs = request.get_json() 181 | ok, msg = store.Store.get().validateEntities(configs) 182 | return {"ok": ok, "msg": msg} 183 | 184 | @app.route('/tables') 185 | def get_table_keys(): 186 | tables = store.Store.get().getNestedKeys() 187 | response = {"tables": tables} 188 | return response 189 | 190 | @app.route('/extra_tables') 191 | def get_extra_table_keys(): 192 | tables = store.Store.get().getNestedKeys(isExtra = True) 193 | response = {"tables": tables} 194 | return response 195 | 196 | @app.route('/values_yt', methods=['POST']) 197 | def get_yt_values(): 198 | field = request.get_json() 199 | table = field['table'] 200 | column = field['column'] 201 | isExtra = field['isExtra'] 202 | columns = list(set([column, "timestamp_tiplot"])) # remove duplicates 203 | err = "no error" 204 | ok = True 205 | if isExtra: 206 | datadict = store.Store.get().extra_datadict 207 | else: 208 | datadict = store.Store.get().datadict 209 | try: 210 | values = datadict[table][columns].fillna(0).to_dict('records') 211 | except Exception as e: 212 | # columns not found 213 | values = [] 214 | err = str(e) 215 | ok = False 216 | response = {"table": table, "column": column , "values": values, "err": err, "ok": ok} 217 | return response 218 | 219 | @app.route('/values_xy', methods=['POST']) 220 | def get_xy_values(): 221 | field = request.get_json() 222 | table = field['table'] 223 | xCol = field['x'] 224 | yCol = field['y'] 225 | columns = [xCol, yCol, "timestamp_tiplot"] 226 | datadict = store.Store.get().datadict 227 | err = "no error" 228 | ok = True 229 | try: 230 | values = datadict[table][columns].fillna(0).to_dict('records') 231 | except Exception as e: 232 | # columns not found 233 | values = [] 234 | err = str(e) 235 | ok = False 236 | response = {"table": table, "x": xCol, "y": yCol, "values": values, "err": err, "ok": ok} 237 | return response 238 | 239 | @app.route('/correlation', methods=['POST']) 240 | def get_correlation_matrix(): 241 | req = request.get_json() 242 | if not req: 243 | return [] 244 | tables = req["tables"] 245 | df_list = [] 246 | for topic in list(tables.keys()): 247 | cols = tables[topic] 248 | cols.append("timestamp_tiplot") 249 | try: 250 | df = store.Store.get().datadict[topic][cols] 251 | df = df.add_prefix(f'{topic}_') 252 | renamed = df.rename(columns={f'{topic}_timestamp_tiplot': "timestamp_tiplot"}) 253 | df_list.append(renamed) 254 | except: 255 | # columns not found 256 | pass 257 | 258 | if (len(df_list) == 0): 259 | return [] 260 | 261 | result = df_list[0] 262 | for i in range(1, len(df_list)): 263 | sorted = df_list[i].sort_values(by='timestamp_tiplot') 264 | result = pd.merge_asof(result, sorted, on='timestamp_tiplot') 265 | 266 | # filter data to include only the zoomed timestamp 267 | if "x_range" in req: 268 | x_range = req["x_range"] 269 | result = result.query('@x_range[0] < timestamp_tiplot < @x_range[1]') 270 | 271 | result = result.drop(columns=["timestamp_tiplot"]) 272 | # corr = result.corr().fillna(-1) 273 | corr = result.corr() 274 | data = json.loads(corr.to_json(orient='split')) 275 | columns = data['columns'] 276 | values = data['data'] 277 | return { 'columns': columns, 'values': values} 278 | 279 | @app.route('/log_files/') 280 | def get_sorted_logs(sort): 281 | sort = sort.lower() 282 | if sort == "time": 283 | files = [(path.basename(x), path.getsize(x), strftime( 284 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'), key=path.getmtime)] 285 | elif sort == "time_desc": 286 | files = [(path.basename(x), path.getsize(x), strftime( 287 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'), key=path.getmtime, reverse=True)] 288 | elif sort == "size": 289 | files = [(path.basename(x), path.getsize(x), strftime( 290 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'), key=path.getsize)] 291 | elif sort == "size_desc": 292 | files = [(path.basename(x), path.getsize(x), strftime( 293 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'), key=path.getsize, reverse=True)] 294 | elif sort == "name": 295 | files = [(path.basename(x), path.getsize(x), strftime( 296 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'))] 297 | elif sort == "name_desc": 298 | files = [(path.basename(x), path.getsize(x), strftime( 299 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in sorted(glob(logs_dir + '/*'), reverse=True)] 300 | else: 301 | files = [(path.basename(x), path.getsize(x), strftime( 302 | '%Y-%m-%d %H:%M:%S', localtime(path.getmtime(x)))) for x in glob(logs_dir + '/*')] 303 | 304 | data = {'path': logs_dir, 'files': files} 305 | return data 306 | 307 | 308 | @app.route('/select_log', methods=['POST']) 309 | def select_log(): 310 | global current_file 311 | file = request.get_json() 312 | current_file = file 313 | ok = choose_parser(file[0], logs_dir) 314 | return {"ok": ok} 315 | 316 | @app.route('/add_log', methods=['POST']) 317 | def add_log(): 318 | file = request.get_json() 319 | ok = choose_parser(file[0], logs_dir, True) 320 | return {"ok": ok} 321 | 322 | 323 | @app.route('/entities_props') 324 | def get_entities_props(): 325 | props, err, ok = store.Store.get().getEntitiesProps() 326 | res = {"data": props, "error": err, "ok": ok} 327 | return res 328 | 329 | @app.route('/current_file') 330 | def get_current_file(): 331 | global current_file 332 | if current_file is None: 333 | return {"msg": "no file selected", "appVersion": args.version} 334 | return {"file": current_file, "appVersion": args.version} 335 | 336 | @app.route('/keys') 337 | def get_keys(): 338 | keys = store.Store.get().getKeys() 339 | response = {"keys": keys} 340 | return response 341 | 342 | 343 | @app.route('/additional_info') 344 | def get_additional_info(): 345 | info = store.Store.get().info 346 | hasExtra = bool(store.Store.get().extra_datadict) 347 | hasMain = bool(store.Store.get().datadict) 348 | res = {"info": info, "hasExtra": hasExtra, "hasMain": hasMain} 349 | return res 350 | 351 | 352 | @app.route('/current_parser') 353 | def get_current_parser(): 354 | global current_parser, current_ext 355 | if (current_parser is None): 356 | parser = "no_parser" 357 | else: 358 | parser = current_parser.name 359 | ext = current_ext or "default" 360 | res = {"parser": parser, "ext": ext} 361 | return res 362 | 363 | @app.route('/merge_extra', methods=['POST']) 364 | def merge_extra(): 365 | res = request.get_json() 366 | prefix = res['prefix'] 367 | delta = float(res['delta']) 368 | # try: 369 | store.Store.get().mergeExtra(prefix, delta) 370 | ok = True 371 | # except: 372 | # ok = False 373 | return {"ok": ok} 374 | 375 | @app.route('/run_sequence', methods=['POST']) 376 | def run_sequence(): 377 | body = request.get_json() 378 | sequence_name = body['sequence'] 379 | sequence_file = sequences_dir + sequence_name 380 | print("Running " + sequence_file) 381 | datadict = store.Store.get().datadict 382 | try: 383 | with open(sequence_file, "r") as f: 384 | code = f.read() 385 | global_namespace = {} 386 | local_namespace = {} 387 | exec(code, global_namespace, local_namespace) 388 | handle_data = local_namespace['handle_data'] 389 | store.Store.get().datadict = handle_data(datadict) 390 | ok = True 391 | err = "no error" 392 | except Exception as e: 393 | err = traceback.format_exc() 394 | ok = False 395 | return {"ok": ok, "err": err} 396 | 397 | @app.route('/sequences') 398 | def get_sequences(): 399 | files = glob(sequences_dir + "/*") 400 | # use the path module to get only the basename of each file 401 | file_names = [path.basename(file) for file in files] 402 | data = {'path': sequences_dir, 'files': file_names} 403 | return data 404 | 405 | @app.route('/create_sequence_file', methods=['POST']) 406 | def create_sequence_file(): 407 | body = request.get_json() 408 | sequence_name = body['name']+".py" 409 | file_path = path.join(sequences_dir, sequence_name) 410 | try: 411 | with open(file_path, 'w') as file: 412 | file.write("""def handle_data(datadict): 413 | import numpy as np 414 | import pandas as pd 415 | 416 | new = datadict 417 | return new""") 418 | ok = True 419 | err = "no error" 420 | except Exception as e: 421 | err = traceback.format_exc() 422 | ok = False 423 | 424 | return {"ok": ok, "err": err} 425 | 426 | @app.route('/open_sequence_file', methods=['POST']) 427 | def open_sequence_file(): 428 | body = request.get_json() 429 | sequence_name = body['sequence'] 430 | sequence_file = sequences_dir + sequence_name 431 | try: 432 | command = body['editorBinary'].split(" ") 433 | command.append(sequence_file) 434 | subprocess.Popen(command) # run the command asyncronously 435 | ok = True 436 | err = "no error" 437 | except: 438 | err = traceback.format_exc() 439 | ok = False 440 | return {"ok": ok, "err": err} 441 | 442 | @socketio.on("disconnect") 443 | def disconnected(): 444 | # print("-> client has disconnected " + request.sid) 445 | pass 446 | 447 | 448 | arg_parser = ArgumentParser(description="Tiplot") 449 | arg_parser.add_argument('--port', type=int, default=5000, help='Port to run the server on') 450 | arg_parser.add_argument('--model', type=str, default= getcwd() + "/../obj/main.gltf", help='Path to the model file') 451 | arg_parser.add_argument('--version', type=str, default="0.0.0-debug", help='App version') 452 | args = arg_parser.parse_args() 453 | 454 | 455 | def print_tiplot(): 456 | print(''' 457 | _____ _ ____ _ _ 458 | |_ _(_) _ \| | ___ | |_ 459 | | | | | |_) | |/ _ \| __| 460 | | | | | __/| | (_) | |_ 461 | |_| |_|_| |_|\___/ \__| 462 | ''') 463 | print(f'-> Starting TiPlot v{args.version} on port {args.port}...') 464 | 465 | def run_server(): 466 | try: 467 | socketio.run(app, host='127.0.0.1', port=args.port) 468 | except: 469 | print('~> Port busy.') 470 | finally: 471 | print('-> See you soon.') 472 | 473 | if __name__ == '__main__': 474 | print_tiplot() 475 | run_server() 476 | -------------------------------------------------------------------------------- /api/store.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import threading 3 | from cesium_entity import CesiumEntity 4 | 5 | class Store: 6 | __instance = None 7 | @staticmethod 8 | def get(): 9 | __lock = threading.Lock() 10 | with __lock: 11 | if Store.__instance is None: 12 | print("~> Store created") 13 | Store() 14 | return Store.__instance 15 | 16 | def __init__(self): 17 | __lock = threading.Lock() 18 | with __lock: 19 | if Store.__instance is not None: 20 | raise Exception("this class is a singleton!") 21 | else: 22 | Store.__instance = self 23 | 24 | self.datadict = {} 25 | self.extra_datadict = {} 26 | self.entities = [] 27 | self.info = [] 28 | self.lock = threading.Lock() 29 | 30 | def getStore(self): 31 | with self.lock: 32 | return {'entities':self.entities, 'datadict':self.datadict} 33 | 34 | def setStore(self,datadict,entities, info=[]): 35 | with self.lock: 36 | self.datadict = datadict 37 | self.entities = entities 38 | self.info = info 39 | 40 | def setExtra(self, datadict): 41 | with self.lock: 42 | self.extra_datadict = datadict 43 | 44 | def getKeys(self): 45 | keys = list(self.datadict.keys()) 46 | return keys 47 | 48 | def getEntitiesProps(self): 49 | data = [] 50 | err = "no error" 51 | ok = True 52 | for e in self.entities: 53 | try: 54 | if (e.position['table'] == e.attitude['table']): 55 | merged = pd.DataFrame.from_dict(self.datadict[e.position['table']]) 56 | else: 57 | merged = pd.merge_asof(self.datadict[e.position['table']], self.datadict[e.attitude['table']], on='timestamp_tiplot', suffixes=('', '_drop')).ffill().bfill() 58 | merged = merged.loc[:, ~merged.columns.str.endswith('_drop')] 59 | if e.useXYZ: 60 | position_columns = [e.position['x'], e.position['y'], e.position['z']] 61 | position_columns_mapped = { e.position['x']: 'x', e.position['y']: 'y', e.position['z']: 'z'} 62 | else: 63 | position_columns = [e.position['altitude'], e.position['lattitude'], e.position['longitude']] 64 | position_columns_mapped = { e.position['longitude']: 'longitude', e.position['altitude']: 'altitude', e.position['lattitude']: 'lattitude'} 65 | if e.useRPY: 66 | attitude_columns = [e.attitude['roll'], e.attitude['pitch'], e.attitude['yaw']] 67 | attitude_columns_mapped = {e.attitude['roll'] : 'roll', e.attitude['pitch'] : 'pitch', e.attitude['yaw'] : 'yaw'} 68 | else: 69 | attitude_columns = [e.attitude['q0'],e.attitude['q1'],e.attitude['q2'], e.attitude['q3']] 70 | attitude_columns_mapped = {e.attitude['q0'] : 'q0', e.attitude['q1'] : 'q1', e.attitude['q2'] : 'q2', e.attitude['q3'] : 'q3'} 71 | columns = position_columns + attitude_columns + ['timestamp_tiplot'] 72 | mapped_columns = {} 73 | mapped_columns.update(position_columns_mapped) 74 | mapped_columns.update(attitude_columns_mapped) 75 | raw = merged[columns] 76 | renamed = raw.rename(columns=mapped_columns).to_dict('records') 77 | data.append({"id": e.id, 78 | "name": e.name, 79 | "alpha": e.alpha, 80 | "scale": e.scale, 81 | "useRPY": e.useRPY, 82 | "useXYZ": e.useXYZ, 83 | "props": renamed, 84 | "color": e.color, 85 | "wireframe": e.wireframe, 86 | "tracked": e.tracked, 87 | "active": e.active, 88 | "pathColor": e.pathColor}) 89 | except Exception as error: 90 | err = str(error) 91 | ok = False 92 | return data, err, ok 93 | 94 | def getEntities(self): 95 | data = [] 96 | for e in self.entities: 97 | data.append(e.toJson()) 98 | return data 99 | 100 | def setEntities(self, entities): 101 | mapped = [] 102 | for entity in entities: 103 | mapped.append(CesiumEntity.fromJson(entity)) 104 | self.entities = mapped 105 | 106 | def validateEntities(self, entities): 107 | for e in entities: 108 | position = e['position'] 109 | attitude = e['attitude'] 110 | if (position['table'] not in self.datadict): 111 | msg = "No table \'" + position['table'] + "\' in the datadict." 112 | return False, msg 113 | if (attitude['table'] not in self.datadict): 114 | msg = "No table \'" + position['table'] + "\' in the datadict." 115 | return False, msg 116 | 117 | if e['useXYZ']: p_columns = ['x', 'y', 'z'] 118 | else: p_columns = ['longitude', 'lattitude', 'altitude'] 119 | 120 | for column in p_columns: 121 | if (position[column] not in self.datadict[position['table']]): 122 | msg = "No column \'" + position[column] + "\' in " + position['table'] 123 | return False, msg 124 | 125 | if e['useRPY']: a_columns = ['roll', 'pitch', 'yaw'] 126 | else: a_columns = ['q0', 'q1', 'q2', 'q3'] 127 | 128 | for column in a_columns: 129 | if (attitude[column] not in self.datadict[attitude['table']]): 130 | msg = "No column \'" + attitude[column] + "\' in " + attitude['table'] 131 | return False, msg 132 | 133 | msg = "config is valid" 134 | return True,msg 135 | 136 | def getTableColumns(self,key): 137 | nested = list(self.datadict[key].keys()) 138 | return nested 139 | 140 | def getNestedKeys(self, isExtra = False): 141 | nested = [] 142 | if isExtra: 143 | for key in self.extra_datadict.keys(): 144 | k = list(self.extra_datadict[key].keys()) 145 | nested.append({key: k}) 146 | else: 147 | for key in self.datadict.keys(): 148 | k = list(self.datadict[key].keys()) 149 | nested.append({key: k}) 150 | return nested 151 | 152 | def mergeExtra(self, prefix, delta): 153 | shifted = {} 154 | for topic, df in self.extra_datadict.items(): 155 | new_df = df.copy() 156 | if "timestamp_tiplot" in new_df: 157 | new_df['timestamp_tiplot'] = new_df['timestamp_tiplot'] + delta 158 | shifted[topic] = new_df 159 | 160 | prefixed = {prefix + key: value for key, value in shifted.items()} 161 | self.datadict = {**self.datadict, **prefixed} 162 | 163 | 164 | -------------------------------------------------------------------------------- /docs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilak-io/tiplot/9dbacac7484fe33914e57ea05caa38472dcc8dd7/docs/demo.png -------------------------------------------------------------------------------- /obj/main.gltf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilak-io/tiplot/9dbacac7484fe33914e57ea05caa38472dcc8dd7/obj/main.gltf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiplot", 3 | "version": "1.2.5", 4 | "private": true, 5 | "homepage": "./", 6 | "proxy": "http://localhost:5000", 7 | "author": "Hamza ZOGHMAR tilak.io", 8 | "description": "Cool and simple visualising tool to analyse your drone flights", 9 | "main": "public/electron.js", 10 | "build": { 11 | "appId": "io.tilak.tiplot", 12 | "extraResources": [ 13 | { 14 | "from": "./backend/", 15 | "to": "api", 16 | "filter": [ 17 | "**/*" 18 | ] 19 | }, 20 | { 21 | "from": "./obj/", 22 | "to": "obj", 23 | "filter": [ 24 | "**/*" 25 | ] 26 | } 27 | ], 28 | "linux": { 29 | "icon": "src/static/img/logo.png", 30 | "category": "Utility" 31 | }, 32 | "win": { 33 | "icon": "src/static/img/logo.png" 34 | } 35 | }, 36 | "dependencies": { 37 | "@testing-library/jest-dom": "^5.16.5", 38 | "@testing-library/react": "^13.4.0", 39 | "@testing-library/user-event": "^14.4.3", 40 | "plotly.js": "^2.17.1", 41 | "portfinder": "^1.0.32", 42 | "react": "^18.2.0", 43 | "react-bootstrap": "^2.5.0", 44 | "react-bootstrap-submenu": "^3.0.1", 45 | "react-dom": "^18.2.0", 46 | "react-grid-layout": "^1.3.4", 47 | "react-icons": "^4.10.1", 48 | "react-new-window": "^1.0.1", 49 | "react-plotly.js": "^2.6.0", 50 | "react-router-dom": "^6.6.2", 51 | "react-scripts": "5.0.1", 52 | "react-select": "^5.4.0", 53 | "react-split-pane": "^0.1.92", 54 | "react-toastify": "^9.1.2", 55 | "socket.io": "^4.5.2", 56 | "socket.io-client": "^4.5.2", 57 | "three": "^0.148.0", 58 | "uuid": "^9.0.0", 59 | "web-vitals": "^3.1.1" 60 | }, 61 | "scripts": { 62 | "start": "react-scripts start", 63 | "build": "react-scripts build", 64 | "test": "react-scripts test", 65 | "eject": "react-scripts eject", 66 | "start:api": "cd api && python3 server.py", 67 | "build:api": "node package_python.js", 68 | "serve:electron": "concurrently -k \"yarn start:api \" \"cross-env BROWSER=none yarn start\" \"yarn start:electron\"", 69 | "build:electron": "yarn build && electron-builder", 70 | "start:electron": "wait-on tcp:3000 && electron .", 71 | "clean": "rm -rf dist/ server.spec main.spec build/ backend" 72 | }, 73 | "eslintConfig": { 74 | "extends": [ 75 | "react-app", 76 | "react-app/jest" 77 | ] 78 | }, 79 | "browserslist": { 80 | "production": [ 81 | ">0.2%", 82 | "not dead", 83 | "not op_mini all" 84 | ], 85 | "development": [ 86 | "last 1 chrome version", 87 | "last 1 firefox version", 88 | "last 1 safari version" 89 | ] 90 | }, 91 | "devDependencies": { 92 | "concurrently": "^7.4.0", 93 | "cross-env": "^7.0.3", 94 | "electron": "^22.0.1", 95 | "electron-builder": "^23.3.3", 96 | "three-orbit-controls": "^82.1.0", 97 | "wait-on": "^7.0.1" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package_python.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const isWin = process.platform === "win32"; 3 | 4 | var add_data; 5 | if (isWin) add_data = "--add-data api;api"; 6 | else 7 | add_data = 8 | "--add-data api:api --add-data $(pip show pymavlink | grep 'Location:' | awk '{print $2}')/pymavlink:pymavlink"; 9 | 10 | const spawn = require("child_process").spawn, 11 | ls = spawn( 12 | "python3 -m PyInstaller", 13 | [ 14 | "-w", 15 | // "--onefile", 16 | "--onedir", 17 | // "--strip", 18 | "--distpath backend", 19 | add_data, 20 | "api/server.py", 21 | ], 22 | { 23 | shell: true, 24 | } 25 | ); 26 | 27 | ls.stderr.on("data", function (data) { 28 | // stream output of build process 29 | console.log(data.toString()); 30 | }); 31 | 32 | ls.on("exit", function (code) { 33 | console.log("child process exited with code " + code.toString()); 34 | }); 35 | -------------------------------------------------------------------------------- /public/electron.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { app, BrowserWindow } = require("electron"); 3 | const portfinder = require("portfinder"); 4 | const api = path.join(process.resourcesPath, "api/server/server"); 5 | const model = path.join(process.resourcesPath, "obj/main.gltf"); 6 | 7 | // just for debugging 8 | // const api = "/home/hamza/projects/github/tiplot/backend/server"; 9 | 10 | var spawn = require("child_process").spawn; 11 | var start; 12 | portfinder.getPort({ port: 5000, stopPort: 6000 }, function (error, port) { 13 | if (error) { 14 | console.error(error); 15 | } else { 16 | console.log(`Found available port: ${port}`); 17 | process.env.API_PORT = port; 18 | start = spawn( 19 | api, 20 | ["--model", model, "--port", port, "--version", app.getVersion()], 21 | { 22 | windowsHide: true, 23 | shell: process.env.ComSpec, 24 | stdio: "inherit", 25 | } 26 | ); 27 | } 28 | }); 29 | 30 | function createWindow() { 31 | // Create the browser window. 32 | const win = new BrowserWindow({ 33 | width: 1600, 34 | height: 900, 35 | // autoHideMenuBar: true, 36 | webPreferences: { 37 | nodeIntegration: true, 38 | preload: path.join(__dirname, "preload.js"), 39 | }, 40 | }); 41 | 42 | win.setMenu(null); 43 | 44 | // and load the index.html of the app. 45 | // win.loadFile("index.html"); 46 | win.loadURL(`file://${path.join(__dirname, "../build/index.html")}`); 47 | // win.loadURL(`http://localhost:3000`); 48 | // Open the DevTools. 49 | // win.webContents.openDevTools({ mode: "detach" }); 50 | win.webContents.setWindowOpenHandler(({ url }) => { 51 | return { 52 | action: "allow", 53 | overrideBrowserWindowOptions: { 54 | autoHideMenuBar: true, 55 | }, 56 | }; 57 | }); 58 | } 59 | 60 | // This method will be called when Electron has finished 61 | // initialization and is ready to create browser windows. 62 | // Some APIs can only be used after this event occurs. 63 | app.whenReady().then(() => { 64 | createWindow(); 65 | 66 | // Ctrl + C console 67 | process.on("SIGINT", (e) => { 68 | start.kill(); 69 | app.quit(); 70 | }); 71 | }); 72 | 73 | // Quit when all windows are closed, except on macOS. There, it's common 74 | // for applications and their menu bar to stay active until the user quits 75 | // explicitly with Cmd + Q. 76 | app.on("window-all-closed", () => { 77 | if (process.platform !== "darwin") { 78 | start.kill(); 79 | app.quit(); 80 | } 81 | }); 82 | 83 | app.on("activate", () => { 84 | if (BrowserWindow.getAllWindows().length === 0) { 85 | createWindow(); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilak-io/tiplot/9dbacac7484fe33914e57ea05caa38472dcc8dd7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | TiPlot 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge } = require("electron"); 2 | 3 | contextBridge.exposeInMainWorld("API_PORT", parseInt(process.env.API_PORT)); 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./static/css/overlay.css"; 2 | import Loader from "./views/Loader"; 3 | import Settings from "./views/Settings"; 4 | import SyncTimestamp from "./views/SyncTimestamp"; 5 | import Test from "./views/Test"; 6 | import React, { useState, useEffect } from "react"; 7 | import { io } from "socket.io-client"; 8 | import { HashRouter as Router, Route, Routes } from "react-router-dom"; 9 | import MainLayout from "./views/layouts/MainLayout"; 10 | import Entities from "./views/Entities"; 11 | import { PORT } from "./static/js/constants"; 12 | 13 | function App() { 14 | const [socketInstance, setSocketInstance] = useState(""); 15 | 16 | useEffect(() => { 17 | const socket = io(`http://localhost:${PORT}/`, { 18 | transports: ["websocket"], 19 | // cors: { 20 | // origin: "http://localhost:3000/", 21 | // }, 22 | }); 23 | 24 | setSocketInstance(socket); 25 | 26 | socket.on("connect", () => { 27 | // console.log("Connected"); 28 | }); 29 | 30 | socket.on("entities_loaded", () => { 31 | console.log("app recieved the signal"); 32 | // navigate("/home"); 33 | }); 34 | 35 | socket.on("disconnect", () => { 36 | // console.log("Disconnected"); 37 | }); 38 | return () => { 39 | socket.off("connect"); 40 | socket.off("disconnect"); 41 | socket.off("entities_loaded"); 42 | }; 43 | // return function cleanup() { 44 | 45 | // socket.disconnect(); 46 | // }; 47 | }, []); 48 | 49 | // return ; 50 | 51 | if (socketInstance === "") return
Loading
; 52 | else 53 | return ( 54 | <> 55 | 56 | 57 | } 61 | /> 62 | } 65 | /> 66 | } /> 67 | } /> 68 | } 72 | /> 73 | } /> 74 | } /> 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /src/components/EntityConfig.js: -------------------------------------------------------------------------------- 1 | import { Form, Button, Row, Col, InputGroup } from "react-bootstrap"; 2 | import { useState, useEffect } from "react"; 3 | import PlotData from "../controllers/PlotData"; 4 | import Select from "react-select"; 5 | 6 | function EntityConfig({ 7 | removeEntity, 8 | eId, 9 | name, 10 | color, 11 | wireframe, 12 | pathColor, 13 | alpha, 14 | useXYZ, 15 | useRPY, 16 | tracked, 17 | active, 18 | scale, 19 | position, 20 | attitude, 21 | }) { 22 | const [_useXYZ, setXYZ] = useState(useXYZ); 23 | const [_useRPY, setRPY] = useState(useRPY); 24 | const [tables, setTables] = useState([]); 25 | const [options, setOptions] = useState([]); 26 | const [positionOptions, setPositionOptions] = useState([]); 27 | const [attitudeOptions, setAttitudeOptions] = useState([]); 28 | const [disabled, setDisabled] = useState(true); 29 | 30 | const mockPlot = new PlotData(0, null); 31 | 32 | useEffect(() => { 33 | getOptions(); 34 | getTables(); 35 | hideUnusedFields(); 36 | setDisabled(!active); 37 | // eslint-disable-next-line 38 | }, []); 39 | 40 | position = position ?? { table: "" }; 41 | attitude = attitude ?? { table: "" }; 42 | 43 | const getTables = async () => { 44 | const tbl = await mockPlot.getTables(); 45 | setTables(tbl); 46 | }; 47 | 48 | const getOptions = async () => { 49 | const opt = await mockPlot.getOptions(); 50 | setOptions(opt); 51 | 52 | // setting default options 53 | const p_opt = opt.filter((o) => o.value.table === position["table"]); 54 | const p_mapped = p_opt.map((o) => { 55 | return { value: o.value, label: o.value.column }; 56 | }); 57 | setPositionOptions(p_mapped); 58 | 59 | const a_opt = opt.filter((o) => o.value.table === attitude["table"]); 60 | const a_mapped = a_opt.map((o) => { 61 | return { value: o.value, label: o.value.column }; 62 | }); 63 | setAttitudeOptions(a_mapped); 64 | }; 65 | 66 | const handlePositionTableSelect = (e) => { 67 | const opt = options.filter((o) => o.value.table === e.value.table); 68 | const mapped = opt.map((o) => { 69 | return { value: o.value, label: o.value.column }; 70 | }); 71 | setPositionOptions(mapped); 72 | }; 73 | 74 | const handleAttitudeTableSelect = (e) => { 75 | const opt = options.filter((o) => o.value.table === e.value.table); 76 | const mapped = opt.map((o) => { 77 | return { value: o.value, label: o.value.column }; 78 | }); 79 | setAttitudeOptions(mapped); 80 | }; 81 | 82 | const handlePositionTypeChanged = () => { 83 | const xyzContainer = document.getElementById(`XYZ-${eId}`); 84 | const coordinatesContainer = document.getElementById(`Coordinates-${eId}`); 85 | xyzContainer.style.display = _useXYZ ? "none" : "block"; 86 | coordinatesContainer.style.display = _useXYZ ? "block" : "none"; 87 | setXYZ(!_useXYZ); 88 | }; 89 | 90 | const handleAttitudeTypeChanged = () => { 91 | const rpyContainer = document.getElementById(`RPY-${eId}`); 92 | const quaternionsContainer = document.getElementById(`Quaternions-${eId}`); 93 | rpyContainer.style.display = _useRPY ? "none" : "block"; 94 | quaternionsContainer.style.display = _useRPY ? "block" : "none"; 95 | setRPY(!_useRPY); 96 | }; 97 | 98 | const hideUnusedFields = () => { 99 | const xyzContainer = document.getElementById(`XYZ-${eId}`); 100 | const coordinatesContainer = document.getElementById(`Coordinates-${eId}`); 101 | const rpyContainer = document.getElementById(`RPY-${eId}`); 102 | const quaternionsContainer = document.getElementById(`Quaternions-${eId}`); 103 | 104 | rpyContainer.style.display = useRPY ? "block" : "none"; 105 | quaternionsContainer.style.display = useRPY ? "none" : "block"; 106 | xyzContainer.style.display = useXYZ ? "block" : "none"; 107 | coordinatesContainer.style.display = useXYZ ? "none" : "block"; 108 | }; 109 | 110 | const handleActiveChange = (e) => { 111 | const checked = e.target.checked; 112 | setDisabled(!checked); 113 | }; 114 | 115 | return ( 116 |
117 | 118 | 119 | • Entity 🛩️ 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Name 129 | 136 | 137 | 138 | 139 | 140 | Color 141 | 148 | 149 | 150 | 151 | 152 | Opacity 153 | 163 | 164 | 165 | 166 | 167 | Scale 168 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | Path Color 185 | 192 | 193 | 194 | 195 | 196 | 203 | 204 | 205 | 206 | 214 | 215 | 216 | 217 | 224 | 225 | 226 |
227 | 228 | • Position 229 | 255 | 256 | 257 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 300 | 301 | 302 | 325 | 326 | 334 | 335 | 336 | 337 | 353 | 354 | 355 | 375 | 376 | 377 | 393 | 394 | 395 | ({ ...base, zIndex: 9999, fontSize: "75%" }), 183 | }} 184 | /> 185 |
186 |
187 | 232 | 233 |
234 |
235 | ); 236 | } 237 | export default Graph; 238 | -------------------------------------------------------------------------------- /src/components/GraphOptions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { MdZoomOutMap } from "react-icons/md"; 3 | import { BsTrash } from "react-icons/bs"; 4 | import { HiOutlineTicket } from "react-icons/hi"; 5 | import { AiOutlineDotChart, AiOutlineLineChart } from "react-icons/ai"; 6 | import { TbChartDots } from "react-icons/tb"; 7 | import { RiDragMove2Line } from "react-icons/ri"; 8 | import Plotly from "plotly.js/dist/plotly"; 9 | 10 | function GraphOptions({ plotId, id, removeGraph }) { 11 | const [showLegend, setShowLegend] = useState(false); 12 | const [plotType, setPlotType] = useState(1); 13 | const [legendAnchor, setLegendAnchor] = useState(0); 14 | // const [rslider, setRSlider] = useState(true); 15 | // const [select, setSelect] = useState(false); 16 | 17 | useEffect(() => { 18 | document.addEventListener("keydown", setupKeyControls); 19 | // eslint-disable-next-line 20 | }, []); 21 | 22 | const setupKeyControls = (e) => { 23 | if (!e.ctrlKey) return; 24 | switch (e.code) { 25 | case "KeyD": 26 | document.getElementById(`select-${id}`).style.display = "block"; 27 | document.getElementById(`whiteout-${id}`).style.display = "none"; 28 | // setSelect(false); 29 | break; 30 | case "KeyS": 31 | document.getElementById(`select-${id}`).style.display = "none"; 32 | document.getElementById(`whiteout-${id}`).style.display = "block"; 33 | // setSelect(true); 34 | break; 35 | default: 36 | // console.log(e); 37 | break; 38 | } 39 | }; 40 | 41 | const toggleLegend = () => { 42 | var update; 43 | 44 | switch (legendAnchor) { 45 | case 0: 46 | update = { 47 | showlegend: true, 48 | legend: { 49 | font: { 50 | size: 10, 51 | }, 52 | xanchor: "right", 53 | x: 1, 54 | }, 55 | }; 56 | break; 57 | case 1: 58 | update = { 59 | showlegend: true, 60 | legend: { 61 | font: { 62 | size: 10, 63 | }, 64 | xanchor: "left", 65 | x: 0, 66 | }, 67 | }; 68 | break; 69 | case 2: 70 | default: 71 | update = { 72 | showlegend: false, 73 | }; 74 | break; 75 | } 76 | 77 | const plot = document.getElementById(plotId); 78 | Plotly.relayout(plot, update); 79 | 80 | if (legendAnchor >= 2) { 81 | setLegendAnchor(0); 82 | setShowLegend(false); 83 | } else { 84 | setLegendAnchor(legendAnchor + 1); 85 | setShowLegend(true); 86 | } 87 | }; 88 | 89 | const autoscale = () => { 90 | const update = { 91 | "xaxis.autorange": true, 92 | "yaxis.autorange": true, 93 | }; 94 | const plot = document.getElementById(plotId); 95 | Plotly.relayout(plot, update); 96 | }; 97 | 98 | const toggleScatter = () => { 99 | var update; 100 | switch (plotType) { 101 | case 0: 102 | default: 103 | update = { 104 | mode: "line", 105 | }; 106 | break; 107 | case 1: 108 | update = { 109 | mode: "lines+markers", 110 | }; 111 | break; 112 | case 2: 113 | update = { 114 | mode: "markers", 115 | }; 116 | break; 117 | } 118 | const plot = document.getElementById(plotId); 119 | Plotly.restyle(plot, update); 120 | 121 | if (plotType >= 2) setPlotType(0); 122 | else setPlotType(plotType + 1); 123 | }; 124 | 125 | function ToggleScatterIcon() { 126 | var icon; 127 | switch (plotType) { 128 | case 0: 129 | icon = ; 130 | break; 131 | case 1: 132 | icon = ; 133 | break; 134 | case 2: 135 | icon = ; 136 | break; 137 | default: 138 | icon = ; 139 | break; 140 | } 141 | return icon; 142 | } 143 | 144 | // const toggleRangeslider = () => { 145 | // setRSlider(!rslider); 146 | // const rs = rslider ? {} : false; 147 | // const update = { 148 | // xaxis: { rangeslider: rs }, 149 | // }; 150 | // const plot = document.getElementById(plotId); 151 | 152 | // Plotly.relayout(plot, update); 153 | // }; 154 | 155 | // const toggleSelect = () => { 156 | // const s = document.getElementById(`select-${id}`); 157 | // const w = document.getElementById(`whiteout-${id}`); 158 | // s.style.display = select ? "block" : "none"; 159 | // w.style.display = !select ? "block" : "none"; 160 | // setSelect(!select); 161 | // }; 162 | 163 | return ( 164 |
165 | removeGraph(id)} title="Remove Graph"> 166 | 167 | 168 | 169 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | {/* */} 190 | {/* */} 193 | {/* */} 194 |
195 | ); 196 | } 197 | export default GraphOptions; 198 | -------------------------------------------------------------------------------- /src/components/GraphXY.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import Select from "react-select"; 3 | import Plot from "react-plotly.js"; 4 | import PlotData from "../controllers/PlotData"; 5 | import GraphOptions from "./GraphOptions"; 6 | 7 | function GraphXY({ id, updateKeys, initialKeys, removeGraph, autoFocusSelect = false }) { 8 | const plotData = new PlotData(id, initialKeys); 9 | const [options_x, setOptionsX] = useState([]); 10 | const [options_y, setOptionsY] = useState([]); 11 | const [selected_x, setSelectedX] = useState(null); 12 | const [selected_y, setSelectedY] = useState(null); 13 | const [data, setData] = useState([]); 14 | const selectRef = useRef(null); 15 | 16 | useEffect(() => { 17 | getInitialData(); 18 | getOptions(); 19 | const plot = document.getElementById(`plot-${id}`); 20 | new ResizeObserver(stretchHeight).observe(plot); 21 | // eslint-disable-next-line 22 | }, []); 23 | 24 | useEffect(() => { 25 | if (autoFocusSelect && selectRef.current) { 26 | selectRef.current.focus(); 27 | } 28 | }, [autoFocusSelect]); 29 | 30 | const getInitialData = async () => { 31 | if (initialKeys == null) return; 32 | if (initialKeys.length !== 2) return; 33 | setSelectedX(initialKeys[0]); 34 | setSelectedY(initialKeys[1]); 35 | addData(initialKeys[0].value, initialKeys[1].value); 36 | }; 37 | 38 | const stretchHeight = () => { 39 | plotData.stretchHeight(); 40 | }; 41 | 42 | const getOptions = async () => { 43 | const opt = await plotData.getOptions(); 44 | setOptionsX(opt); 45 | }; 46 | 47 | const handleChangeX = (e) => { 48 | setSelectedX(e); 49 | const opt = options_x.filter((o) => o.value.table === e.value.table); 50 | setOptionsY(opt); 51 | 52 | if (selected_y == null) return; 53 | if (selected_y.value.table === e.value.table) { 54 | addData(e.value, selected_y.value); 55 | updateKeys(id, [e, selected_y]); 56 | } else { 57 | setSelectedY(null); 58 | } 59 | }; 60 | 61 | const handleChangeY = (e) => { 62 | setSelectedY(e); 63 | addData(selected_x.value, e.value); 64 | updateKeys(id, [selected_x, e]); 65 | }; 66 | 67 | const addData = async (x, y) => { 68 | const d = await plotData.getDataXY(x, y); 69 | setData([d]); 70 | }; 71 | 72 | const handleHover = (event) => { 73 | const index = event.points[0].pointIndex; 74 | const data = event.points[0].data; 75 | const x = data.t[index]; 76 | if (event.event.altKey) { 77 | plotData.updateTimelineIndicator(x); 78 | } 79 | }; 80 | 81 | return ( 82 |
83 | ({ ...base, zIndex: 9999, fontSize: "75%" }), 102 | }} 103 | /> 104 |
105 | 135 | 136 |
137 |
138 | ); 139 | } 140 | export default GraphXY; 141 | -------------------------------------------------------------------------------- /src/components/Heatmap.js: -------------------------------------------------------------------------------- 1 | import Select from "react-select"; 2 | import Plot from "react-plotly.js"; 3 | import { useState, useEffect, useRef } from "react"; 4 | import GraphOptions from "./GraphOptions"; 5 | import PlotData from "../controllers/PlotData"; 6 | 7 | function Heatmap({ id, initialKeys, updateKeys, removeGraph, autoFocusSelect = false }) { 8 | const plotData = new PlotData(id, initialKeys); 9 | const [options, setOptions] = useState([]); 10 | const [selectedOptions, setSelectedOptions] = useState([]); 11 | const [inputValue, setInputValue] = useState(""); 12 | const [data, setData] = useState([]); 13 | const selectRef = useRef(null); 14 | 15 | useEffect(() => { 16 | getInitialData(); 17 | getOptions(); 18 | const plot = document.getElementById(`plot-${id}`); 19 | new ResizeObserver(stretchHeight).observe(plot); 20 | // eslint-disable-next-line 21 | }, []); 22 | 23 | useEffect(() => { 24 | if (autoFocusSelect && selectRef.current) { 25 | selectRef.current.focus(); 26 | } 27 | }, [autoFocusSelect]); 28 | 29 | const stretchHeight = () => { 30 | plotData.stretchHeight(); 31 | }; 32 | 33 | const getInitialData = async () => { 34 | if (initialKeys == null) return; 35 | if (initialKeys.length === 0) return; 36 | setSelectedOptions(initialKeys); 37 | const d = await plotData.getCorrMatrix(initialKeys); 38 | setData([d]); 39 | squeezeSelect(); 40 | }; 41 | 42 | const getOptions = async () => { 43 | const opt = await plotData.getOptions(); 44 | setOptions(opt); 45 | }; 46 | 47 | const getCorrMatrix = async (fields) => { 48 | const d = await plotData.getCorrMatrix(fields); 49 | setData([d]); 50 | }; 51 | 52 | const handleSelectChange = (keysList, actionMeta) => { 53 | setSelectedOptions(keysList); 54 | updateKeys(id, keysList); 55 | switch (actionMeta.action) { 56 | case "select-option": 57 | case "remove-value": 58 | case "pop-value": 59 | getCorrMatrix(keysList); 60 | break; 61 | case "clear": 62 | setData([]); 63 | break; 64 | default: 65 | break; 66 | } 67 | }; 68 | 69 | const handleInput = (value, event) => { 70 | setInputValue(value); 71 | if (event.action === "set-value") setInputValue(event.prevInputValue); 72 | }; 73 | 74 | const handleRelayout = (event) => { 75 | // only used for updating the correlation matrix if the zoomed x_range has changed 76 | if (!event["x_rangeChanged"]) return; 77 | getCorrMatrix(selectedOptions); 78 | }; 79 | 80 | const squeezeSelect = () => { 81 | const select = document.querySelector( 82 | `#select-${id} > div > div:first-child` 83 | ); 84 | if (select == null) { 85 | return; 86 | } 87 | select.style.maxHeight = "36px"; 88 | }; 89 | 90 | const stretchSelect = () => { 91 | const select = document.querySelector( 92 | `#select-${id} > div > div:first-child` 93 | ); 94 | if (select == null) { 95 | return; 96 | } 97 | select.style.maxHeight = "500px"; 98 | }; 99 | 100 | const isOptionSelected = (option) => { 101 | const labels = selectedOptions.map((e) => e.label); 102 | return labels.includes(option.label); 103 | }; 104 | 105 | return ( 106 |
107 | 154 | 155 | 156 |
157 |
158 |
159 |
160 | 161 | 162 | 163 | 169 | 176 | 183 | 190 | 191 | {files.map((file, i) => { 192 | return ( 193 | parse(file)} 197 | > 198 | 201 | 202 | 203 | 204 | 205 | ); 206 | })} 207 | 208 |
sortFiles("unsorted")} 166 | > 167 | 168 | sortFiles("name")} 172 | > 173 | 174 | {logsDir} 175 | sortFiles("size")} 179 | > 180 | 181 | Size 182 | sortFiles("time")} 186 | > 187 | 188 | Modified 189 |
199 | 200 | {file[0]}{formatFileSize(file[1])}{file[2]}
209 |
210 |
211 |
212 |
213 | 214 | ); 215 | } 216 | 217 | export default Loader; 218 | -------------------------------------------------------------------------------- /src/views/Settings.js: -------------------------------------------------------------------------------- 1 | import ToolBar from "../components/ToolBar"; 2 | import { Container, Form, Row, Col, InputGroup } from "react-bootstrap"; 3 | import { useEffect } from "react"; 4 | import "../static/css/settings.css"; 5 | 6 | export const defaultSettings = { 7 | backgroundColor: "#3b3b3b", 8 | originHelper: false, 9 | xGrid: false, 10 | yGrid: false, 11 | zGrid: false, 12 | maxDistance: 1500, 13 | dampingFactor: 0.8, 14 | fov: 75, 15 | textYValue: 0, 16 | externalEditor: "/usr/bin/code" 17 | }; 18 | 19 | function Settings() { 20 | useEffect(() => { 21 | getCurrentSettings(); 22 | getCurrentLayout(); 23 | getCurrentTrackedEntityType(); 24 | // eslint-disable-next-line 25 | }, []); 26 | 27 | const parseLocalStorage = (key) => { 28 | var value = localStorage.getItem(key); 29 | if (value === "" || value === null) value = defaultSettings; 30 | else value = JSON.parse(value); 31 | return value; 32 | }; 33 | 34 | const getCurrentSettings = () => { 35 | const general_settings = parseLocalStorage("general_settings"); 36 | const keys = Object.keys(defaultSettings); 37 | keys.forEach((key) => { 38 | const input = document.getElementById(key); 39 | if (input.type === "checkbox") 40 | input.checked = general_settings[key] ?? defaultSettings[key]; 41 | else input.value = general_settings[key] ?? defaultSettings[key]; 42 | }); 43 | }; 44 | 45 | const handleChange = (e) => { 46 | const general_settings = parseLocalStorage("general_settings"); 47 | if (e.target.type === "checkbox") 48 | general_settings[e.target.id] = e.target.checked; 49 | else if (e.target.type === "number") 50 | general_settings[e.target.id] = parseFloat(e.target.value) ?? 1; 51 | else general_settings[e.target.id] = e.target.value; 52 | localStorage.setItem("general_settings", JSON.stringify(general_settings)); 53 | }; 54 | 55 | const handleLayoutChange = (e) => { 56 | var view_layout = localStorage.getItem("view_layout", "split-fit"); 57 | view_layout = e.target.id; 58 | localStorage.setItem("view_layout", JSON.stringify(view_layout)); 59 | }; 60 | 61 | const handleTrackedEntityChange = (e) => { 62 | var tracked_entity_type = localStorage.getItem("tracked_entity_type", "last-tracked"); 63 | tracked_entity_type = e.target.id; 64 | localStorage.setItem("tracked_entity_type", JSON.stringify(tracked_entity_type)); 65 | }; 66 | 67 | const getCurrentLayout = () => { 68 | var view_layout = JSON.parse(localStorage.getItem("view_layout")) ?? "split-fit"; 69 | const layouts = ["split-fit", "detached-fit"]; 70 | 71 | layouts.forEach((layout) => { 72 | const input = document.getElementById(layout); 73 | input.checked = view_layout === input.id; 74 | }); 75 | }; 76 | 77 | const getCurrentTrackedEntityType = () => { 78 | var tracked_entity_type = JSON.parse(localStorage.getItem("tracked_entity_type")) ?? "last-tracked"; 79 | const types = ["last-tracked", "last-created"]; 80 | 81 | types.forEach((t) => { 82 | const input = document.getElementById(t); 83 | input.checked = tracked_entity_type === input.id; 84 | }); 85 | }; 86 | 87 | return ( 88 | <> 89 | 90 | 91 |
92 |
93 | • General ⚙️ 94 | 95 | External Editor 96 | 103 | 104 |
105 |
106 | • View Layouts 🪟 107 | 114 | 121 |
122 |
123 | • Camera 📸 124 | 125 | 126 | 127 | Max Distance 128 | 135 | 136 | 137 | 138 | 139 | Damping Factor 140 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | FOV 155 | 160 | 161 | 162 | 163 |
164 | 165 |
166 | • Plots 📈 167 | 168 | 169 | 170 | Text Y Value 171 | 177 | 178 | 179 | 180 |
181 |
182 | • Entities 🛩️ 183 | 190 | 197 |
198 |
199 | • View Helpers 🌎 200 | 206 | 212 | 218 | 224 |
225 | 226 | 227 | Background Color 228 | 229 | 236 | 237 |
238 |
239 |
240 | 241 | ); 242 | } 243 | 244 | export default Settings; 245 | -------------------------------------------------------------------------------- /src/views/SyncTimestamp.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Select from "react-select"; 3 | import Plot from "react-plotly.js"; 4 | import { Container, Form, Row, Col, Button } from "react-bootstrap"; 5 | import ToolBar from "../components/ToolBar"; 6 | import PlotData from "../controllers/PlotData"; 7 | import { PORT } from "../static/js/constants"; 8 | import { useNavigate } from "react-router-dom"; 9 | import Plotly from "plotly.js/dist/plotly"; 10 | import { debounce } from "lodash"; 11 | 12 | function SyncTimestamp() { 13 | const plotData = new PlotData("sync-plot", []); 14 | 15 | const [mainOptions, setMainOptions] = useState([]); 16 | const [mainData, setMainData] = useState({}); 17 | const [extraOptions, setExtraOptions] = useState([]); 18 | const [extraData, setExtraData] = useState({}); 19 | const [shiftedData, setShiftedData] = useState({}); 20 | const [data, setData] = useState([]); 21 | const [delta, setDelta] = useState(0); 22 | const [xaxis, setXAxis] = useState(null); 23 | const [zoomed, setZoomed] = useState(false); 24 | const [syncType, setSyncType] = useState("custom"); 25 | const [range, setRange] = useState(1000); 26 | const [step, setStep] = useState(1); 27 | const [prefix, setPrefix] = useState("extra_"); 28 | const navigate = useNavigate(); 29 | 30 | useEffect(() => { 31 | getOptions(); 32 | setupControls(); 33 | // eslint-disable-next-line 34 | }, []); 35 | 36 | useEffect(() => { 37 | setData([mainData, shiftedData]); 38 | }, [mainData, shiftedData]); 39 | 40 | useEffect(() => { 41 | debouncedShiftTimestamp(delta); 42 | // eslint-disable-next-line 43 | }, [delta, extraData]); 44 | 45 | useEffect(() => { 46 | calculateRange(); 47 | switch (syncType) { 48 | case "first-change": 49 | if ("x" in mainData && "x" in extraData) { 50 | const main_x = findFirstChange(mainData); 51 | const extra_x = findFirstChange(extraData); 52 | setDelta(main_x - extra_x); 53 | updateXAxis(); 54 | } 55 | break; 56 | 57 | case "first-point": 58 | if ("x" in mainData && "x" in extraData) { 59 | const main_x = mainData.x[0]; 60 | const extra_x = extraData.x[0]; 61 | setDelta(main_x - extra_x); 62 | updateXAxis(); 63 | } 64 | break; 65 | 66 | case "closest-point": 67 | if ("y" in mainData && "y" in extraData) { 68 | const extra_y = extraData.y[0]; 69 | const extra_x = extraData.x[0]; // Time reference 70 | console.log("Extra Y", extra_y); 71 | // Find time whe,e we have the closest value of main_y in extra_y 72 | console.log("Closest is", mainData.x[findClosestIndex(mainData.y, extra_y)]); 73 | const main_x = mainData.x[findClosestIndex(mainData.y, extra_y)]; 74 | 75 | setDelta(main_x - extra_x); 76 | updateXAxis(); 77 | } 78 | break; 79 | 80 | case "back-to-back": 81 | case "btb-inversed": 82 | const inv = document.getElementById("btb-inversed").checked; 83 | 84 | if (syncType === "back-to-back" && inv) { 85 | setSyncType("btb-inversed"); 86 | return; 87 | } 88 | if ("x" in mainData && "x" in extraData) { 89 | if (inv) { 90 | const main_x = mainData.x[0]; 91 | const extra_x = extraData.x[extraData.x.length - 1]; 92 | setDelta(main_x - extra_x); 93 | } else { 94 | const main_x = mainData.x[mainData.x.length - 1]; 95 | const extra_x = extraData.x[0]; 96 | setDelta(main_x - extra_x); 97 | } 98 | updateXAxis(); 99 | } 100 | break; 101 | case "custom": 102 | default: 103 | break; 104 | } 105 | // eslint-disable-next-line 106 | }, [syncType, mainData, extraData]); 107 | 108 | const getOptions = async () => { 109 | const m_opt = await plotData.getOptions(false); 110 | setMainOptions(m_opt); 111 | 112 | const e_opt = await plotData.getOptions(true); 113 | setExtraOptions(e_opt); 114 | }; 115 | 116 | const getMainData = async (field) => { 117 | const d = await plotData.getData(field, false); 118 | setXAxis([d.x[0], d.x[d.x.length - 1]]); 119 | setMainData(d); 120 | }; 121 | 122 | const getExtraData = async (field) => { 123 | const d = await plotData.getData(field, true); 124 | setExtraData(d); 125 | setShiftedData(d); 126 | updateXAxis(); 127 | }; 128 | 129 | const debouncedShiftTimestamp = debounce((_delta) => { 130 | const dt = parseFloat(_delta); 131 | const ed = Object.assign({}, extraData); 132 | if ("x" in ed) { 133 | ed.x = ed.x.map((t) => t + dt); 134 | setShiftedData(ed); 135 | } 136 | updateXAxis(); 137 | }, 50); 138 | 139 | const updateXAxis = () => { 140 | if (zoomed) return; 141 | if ("x" in mainData && "x" in shiftedData) { 142 | const main_x = mainData.x; 143 | const shifted_x = shiftedData.x; 144 | 145 | const x0 = Math.min(main_x[0], shifted_x[0]); 146 | const x1 = Math.max( 147 | main_x[main_x.length - 1], 148 | shifted_x[shifted_x.length - 1] 149 | ); 150 | setXAxis([x0, x1]); 151 | } 152 | }; 153 | 154 | const handleRelayout = (event) => { 155 | if (event["xaxis.autorange"] === true) { 156 | setZoomed(false); 157 | } else { 158 | setZoomed(true); 159 | } 160 | }; 161 | 162 | const handleClick = (event) => { 163 | // TODO: Add setting delta with the diff between two points 164 | if (event.event.ctrlKey) { 165 | console.log(event); 166 | } 167 | }; 168 | 169 | const handleRadioChange = (event) => { 170 | var selected = event.target.id; 171 | setSyncType(selected); 172 | setTimeout(function() { 173 | autoRange(); 174 | }, 200); 175 | }; 176 | 177 | const handleInversedCheck = (event) => { 178 | var selected = event.target.id; 179 | if (event.target.checked) setSyncType(selected); 180 | else setSyncType("back-to-back"); 181 | setTimeout(function() { 182 | autoRange(); 183 | }, 200); 184 | }; 185 | 186 | const findFirstChange = (data) => { 187 | for (let i = 0; i < data.y.length - 1; i++) { 188 | if (data.y[i] !== data.y[i + 1]) { 189 | return data.x[i]; 190 | } 191 | } 192 | return data.x[0]; 193 | }; 194 | 195 | const findClosestIndex = (arr, value) => { 196 | let minDiff = Number.MAX_VALUE; 197 | let closestIndex = -1; 198 | 199 | for (let i = 0; i < arr.length; i++) { 200 | const diff = Math.abs(arr[i] - value); 201 | if (diff < minDiff) { 202 | minDiff = diff; 203 | closestIndex = i; 204 | } 205 | } 206 | return closestIndex; 207 | } 208 | 209 | const handleApply = async () => { 210 | const req = { 211 | prefix: prefix, 212 | delta: delta, 213 | }; 214 | 215 | await fetch(`http://localhost:${PORT}/merge_extra`, { 216 | headers: { 217 | "Content-Type": "application/json", 218 | }, 219 | method: "POST", 220 | body: JSON.stringify(req), 221 | }) 222 | .then((res) => res.json()) 223 | .then((res) => { 224 | if (res.ok) navigate("/home"); 225 | else alert("Error: Can't merge datadicts"); 226 | }); 227 | }; 228 | 229 | const calculateRange = () => { 230 | if ("x" in mainData && "x" in extraData) { 231 | let min = Math.min(mainData.x[0], extraData.x[0]); 232 | let max = Math.max( 233 | mainData.x[mainData.x.length - 1], 234 | extraData.x[extraData.x.length - 1] 235 | ); 236 | setRange(max - min); 237 | 238 | let stp = mainData.x[1] - mainData.x[0]; 239 | setStep(stp); 240 | } 241 | 242 | setTimeout(function() { 243 | autoRange(); 244 | }, 200); 245 | }; 246 | 247 | const autoRange = () => { 248 | const plot = document.getElementById("sync-plot"); 249 | const update = { 250 | "xaxis.autorange": true, 251 | "yaxis.autorange": true, 252 | }; 253 | Plotly.relayout(plot, update); 254 | }; 255 | 256 | const setupControls = () => { 257 | // TODO: Add Keyboard controls 258 | document.onkeyup = function(e) { 259 | switch (e.code) { 260 | case "ArrowRight": 261 | case "ArrowLeft": 262 | default: 263 | break; 264 | } 265 | }; 266 | }; 267 | 268 | return ( 269 | <> 270 | 271 |
272 | 273 | 274 | 275 | setPrefix(e.target.value)} 279 | /> 280 | 281 | 282 | setDelta(e.target.value)} 288 | disabled={syncType !== "custom"} 289 | /> 290 | 291 | 292 | setDelta(e.target.value)} 295 | step={step} 296 | min={-range} 297 | max={range} 298 | disabled={syncType !== "custom"} 299 | onMouseUp={autoRange} 300 | /> 301 | 309 | 316 | 323 | 330 |
331 | 339 | 349 |
350 |
351 | getExtraData(e.value)} 360 | /> 361 | 394 | 395 | 396 | 399 | 400 |
401 | 402 | 403 | ); 404 | } 405 | export default SyncTimestamp; 406 | -------------------------------------------------------------------------------- /src/views/Test.js: -------------------------------------------------------------------------------- 1 | import Heatmap from "../components/Heatmap"; 2 | 3 | function Test() { 4 | return {}} />; 5 | } 6 | 7 | export default Test; 8 | -------------------------------------------------------------------------------- /src/views/layouts/DetachedLayout.js: -------------------------------------------------------------------------------- 1 | import "../../../node_modules/react-grid-layout/css/styles.css"; 2 | import "../../static/css/layout.css"; 3 | import { useState, useEffect } from "react"; 4 | import RGL, { WidthProvider } from "react-grid-layout"; 5 | import { v4 as uuid } from "uuid"; 6 | import ToolBar from "../../components/ToolBar"; 7 | import Graph from "../../components/Graph"; 8 | import GraphXY from "../../components/GraphXY"; 9 | import View3D from "../../components/View3D"; 10 | import NewWindow from "react-new-window"; 11 | import Heatmap from "../../components/Heatmap"; 12 | 13 | const ReactGridLayout = WidthProvider(RGL); 14 | 15 | function DetachedLayout({ socket, defaultShowView, ext }) { 16 | const [graphs, setGraphs] = useState([]); 17 | const [rowHeight, setRowHeight] = useState(null); 18 | const [positions, setPositions] = useState([]); 19 | const [showView, setShowView] = useState(defaultShowView); 20 | 21 | useEffect(() => { 22 | initializeLayout(); 23 | // eslint-disable-next-line 24 | }, []); 25 | 26 | useEffect(() => { 27 | fitToScreen(); 28 | window.addEventListener("resize", fitToScreen); 29 | // eslint-disable-next-line 30 | }, [graphs]); 31 | 32 | const fitToScreen = () => { 33 | var usedHeight = 56; // ToolBar height 34 | setRowHeight((window.innerHeight - usedHeight) / graphs.length); 35 | }; 36 | 37 | const toggle3dView = () => { 38 | setShowView(!showView); 39 | }; 40 | 41 | const addGraphYT = () => { 42 | const id = uuid(); 43 | const graph = ( 44 |
45 | 52 |
53 | ); 54 | setGraphs([...graphs, graph]); 55 | addGraphToLayout("yt", id); 56 | }; 57 | 58 | const addGraphXY = () => { 59 | const id = uuid(); 60 | const graph = ( 61 |
62 | 69 |
70 | ); 71 | setGraphs([...graphs, graph]); 72 | addGraphToLayout("xy", id); 73 | }; 74 | 75 | const addGraphHM = () => { 76 | const id = uuid(); 77 | const graph = ( 78 |
79 | 86 |
87 | ); 88 | setGraphs([...graphs, graph]); 89 | addGraphToLayout("hm", id); 90 | }; 91 | 92 | const updateKeys = (id, keys) => { 93 | var layout = parseLocalStorage("curr_layout"); 94 | const plot = layout[ext].find((p) => p.id === id); 95 | console.log(layout); 96 | plot.keys = keys; 97 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 98 | }; 99 | 100 | const parseLocalStorage = (key) => { 101 | try { 102 | var value = localStorage.getItem(key); 103 | if (value === "" || value === null) { 104 | value = {}; 105 | } else { 106 | value = JSON.parse(value); 107 | } 108 | } catch { 109 | alert("Please import a valid json file"); 110 | localStorage.setItem(key, "{}"); 111 | value = {}; 112 | } 113 | 114 | if (!(ext in value)) { 115 | value[ext] = []; 116 | } 117 | 118 | return value; 119 | }; 120 | 121 | const initializeLayout = () => { 122 | var layout = parseLocalStorage("curr_layout"); 123 | var pos = parseLocalStorage("curr_positions"); 124 | setPositions(pos[ext]); 125 | var g = []; 126 | layout[ext].forEach((p) => { 127 | var graph; 128 | if (p.type === "yt") 129 | graph = ( 130 |
131 | 137 |
138 | ); 139 | if (p.type === "xy") 140 | graph = ( 141 |
142 | 148 |
149 | ); 150 | if (p.type === "hm") 151 | graph = ( 152 |
153 | 159 |
160 | ); 161 | g.push(graph); 162 | }); 163 | setGraphs(g); 164 | }; 165 | 166 | const addGraphToLayout = (type, id) => { 167 | var layout = parseLocalStorage("curr_layout"); 168 | layout[ext].push({ id: id, type: type, keys: [] }); 169 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 170 | }; 171 | 172 | const removeGraph = (id) => { 173 | var layout = parseLocalStorage("curr_layout"); 174 | layout[ext] = layout[ext].filter((graph) => graph.id !== id); 175 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 176 | initializeLayout(); 177 | }; 178 | 179 | const handleLayoutChange = (layout) => { 180 | var pos = parseLocalStorage("curr_positions"); 181 | pos[ext] = layout; 182 | localStorage.setItem("curr_positions", JSON.stringify(pos)); 183 | }; 184 | 185 | const handleToggle = (value) => { 186 | setShowView(value); 187 | localStorage.setItem("show_view", JSON.stringify(value)); 188 | }; 189 | 190 | return ( 191 | <> 192 | 201 |
202 | 212 | {graphs} 213 | 214 |
215 | 216 | 217 | ); 218 | } 219 | 220 | function Detached3D({ show, toggle }) { 221 | return show ? ( 222 | toggle(true)} 227 | onUnload={() => toggle(false)} 228 | > 229 | 230 | 231 | ) : ( 232 |
233 | ); 234 | } 235 | 236 | export default DetachedLayout; 237 | -------------------------------------------------------------------------------- /src/views/layouts/MainLayout.js: -------------------------------------------------------------------------------- 1 | import "../../../node_modules/react-grid-layout/css/styles.css"; 2 | import "../../static/css/layout.css"; 3 | import { useState, useEffect } from "react"; 4 | import SplitLayout from "./SplitLayout"; 5 | import DetachedLayout from "./DetachedLayout"; 6 | import { PORT } from "../../static/js/constants"; 7 | 8 | function MainLayout({ socket }) { 9 | const [selectedLayout, setSelectedLayout] = useState("split-fit"); 10 | const [showView, setShowView] = useState(false); 11 | const [ext, setExt] = useState("default"); 12 | 13 | useEffect(() => { 14 | getExt(); 15 | getSelectedLayout(); 16 | getShowView(); 17 | }, []); 18 | 19 | const getSelectedLayout = () => { 20 | var view_layout = 21 | JSON.parse(localStorage.getItem("view_layout")) ?? "split-fit"; 22 | setSelectedLayout(view_layout); 23 | }; 24 | 25 | const getShowView = () => { 26 | var show_view = JSON.parse(localStorage.getItem("show_view")) ?? false; 27 | setShowView(show_view); 28 | }; 29 | 30 | const getExt = () => { 31 | fetch(`http://localhost:${PORT}/current_parser`) 32 | .then((res) => res.json()) 33 | .then((res) => { 34 | setExt(res.ext); 35 | }); 36 | }; 37 | 38 | function Layout() { 39 | switch (selectedLayout) { 40 | case "detached-fit": 41 | return ( 42 | 47 | ); 48 | case "split-fit": 49 | default: 50 | return ( 51 | 52 | ); 53 | } 54 | } 55 | 56 | return ; 57 | } 58 | 59 | export default MainLayout; 60 | -------------------------------------------------------------------------------- /src/views/layouts/SplitLayout.js: -------------------------------------------------------------------------------- 1 | import "../../../node_modules/react-grid-layout/css/styles.css"; 2 | import "../../static/css/layout.css"; 3 | import { useState, useEffect } from "react"; 4 | import RGL, { WidthProvider } from "react-grid-layout"; 5 | import { v4 as uuid } from "uuid"; 6 | import ToolBar from "../../components/ToolBar"; 7 | import Graph from "../../components/Graph"; 8 | import GraphXY from "../../components/GraphXY"; 9 | import Heatmap from "../../components/Heatmap"; 10 | import View3D from "../../components/View3D"; 11 | import SplitPane from "react-split-pane"; 12 | 13 | const ReactGridLayout = WidthProvider(RGL); 14 | 15 | function SplitLayout({ socket, defaultShowView, ext }) { 16 | const fullSize = window.innerWidth; 17 | const defaultSize = 0.55 * window.innerWidth; // percentage of screen width 18 | 19 | const [graphs, setGraphs] = useState([]); 20 | const [rowHeight, setRowHeight] = useState(null); 21 | const [positions, setPositions] = useState([]); 22 | const [showView, setShowView] = useState(defaultShowView); 23 | const [size, setSize] = useState(defaultShowView ? defaultSize : fullSize); 24 | 25 | useEffect(() => { 26 | initializeLayout(); 27 | // eslint-disable-next-line 28 | }, []); 29 | 30 | useEffect(() => { 31 | fitToScreen(); 32 | window.addEventListener("resize", fitToScreen); 33 | // eslint-disable-next-line 34 | }, [graphs]); 35 | 36 | const fitToScreen = () => { 37 | var usedHeight = 56; // ToolBar height 38 | setRowHeight((window.innerHeight - usedHeight) / graphs.length); 39 | }; 40 | 41 | const toggle3dView = () => { 42 | setShowView(!showView); 43 | if (showView) setSize(fullSize); 44 | else setSize(defaultSize); 45 | }; 46 | 47 | const addGraphYT = () => { 48 | const id = uuid(); 49 | const graph = ( 50 |
51 | 58 |
59 | ); 60 | setGraphs([...graphs, graph]); 61 | addGraphToLayout("yt", id); 62 | }; 63 | 64 | const addGraphXY = () => { 65 | const id = uuid(); 66 | const graph = ( 67 |
68 | 75 |
76 | ); 77 | setGraphs([...graphs, graph]); 78 | addGraphToLayout("xy", id); 79 | }; 80 | 81 | const addGraphHM = () => { 82 | const id = uuid(); 83 | const graph = ( 84 |
85 | 92 |
93 | ); 94 | setGraphs([...graphs, graph]); 95 | addGraphToLayout("hm", id); 96 | }; 97 | 98 | const updateKeys = (id, keys) => { 99 | var layout = parseLocalStorage("curr_layout"); 100 | const plot = layout[ext].find((p) => p.id === id); 101 | plot.keys = keys; 102 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 103 | }; 104 | 105 | const parseLocalStorage = (key) => { 106 | try { 107 | var value = localStorage.getItem(key); 108 | if (value === "" || value === null) { 109 | value = {}; 110 | } else { 111 | value = JSON.parse(value); 112 | } 113 | } catch { 114 | alert("Please import a valid json file"); 115 | localStorage.setItem(key, "{}"); 116 | value = {}; 117 | } 118 | 119 | if (!(ext in value)) { 120 | value[ext] = []; 121 | } 122 | 123 | return value; 124 | }; 125 | 126 | const initializeLayout = () => { 127 | var layout = parseLocalStorage("curr_layout"); 128 | var pos = parseLocalStorage("curr_positions"); 129 | setPositions(pos[ext]); 130 | var g = []; 131 | layout[ext].forEach((p) => { 132 | var graph; 133 | if (p.type === "yt") 134 | graph = ( 135 |
136 | 142 |
143 | ); 144 | if (p.type === "xy") 145 | graph = ( 146 |
147 | 153 |
154 | ); 155 | if (p.type === "hm") 156 | graph = ( 157 |
158 | 164 |
165 | ); 166 | g.push(graph); 167 | }); 168 | setGraphs(g); 169 | }; 170 | 171 | const addGraphToLayout = (type, id) => { 172 | var layout = parseLocalStorage("curr_layout"); 173 | layout[ext].push({ id: id, type: type, keys: [] }); 174 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 175 | }; 176 | 177 | const removeGraph = (id) => { 178 | var layout = parseLocalStorage("curr_layout"); 179 | layout[ext] = layout[ext].filter((graph) => graph.id !== id); 180 | localStorage.setItem("curr_layout", JSON.stringify(layout)); 181 | initializeLayout(); 182 | }; 183 | 184 | const handleLayoutChange = (layout) => { 185 | var pos = parseLocalStorage("curr_positions"); 186 | pos[ext] = layout; 187 | localStorage.setItem("curr_positions", JSON.stringify(pos)); 188 | }; 189 | 190 | return ( 191 | <> 192 | 201 | 202 |
203 | 213 | {graphs} 214 | 215 |
216 | 217 |
218 | 219 | ); 220 | } 221 | 222 | export default SplitLayout; 223 | -------------------------------------------------------------------------------- /templates/logged_messages.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "e9d557d5", 6 | "metadata": {}, 7 | "source": [ 8 | "## Imports" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "id": "02b9bde1", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import zmq\n", 19 | "import zlib\n", 20 | "import pyulog\n", 21 | "import pandas as pd\n", 22 | "import numpy as np\n", 23 | "import pickle\n", 24 | "from datetime import datetime" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "695b03a1", 30 | "metadata": {}, 31 | "source": [ 32 | "## Socket config" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 2, 38 | "id": "28e028cf", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "port = '5555'\n", 43 | "context = zmq.Context()\n", 44 | "socket = context.socket(zmq.REQ)\n", 45 | "socket.connect(\"tcp://0.0.0.0:%s\" % port)\n", 46 | "\n", 47 | "def send_zipped_pickle(socket, obj, flags=0, protocol=-1):\n", 48 | " p = pickle.dumps(obj, protocol)\n", 49 | " z = zlib.compress(p)\n", 50 | " return socket.send(z, flags=flags)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "id": "3b62481a", 56 | "metadata": {}, 57 | "source": [ 58 | "## Parser" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 3, 64 | "id": "7999d103", 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "ulg = None\n", 69 | "def parse_file(filename):\n", 70 | " global ulg\n", 71 | " ulg = pyulog.ULog(filename)\n", 72 | "\n", 73 | " datadict = {}\n", 74 | " modified_data = {}\n", 75 | " for data in ulg.data_list:\n", 76 | " if data.multi_id > 0:\n", 77 | " name = f\"{data.name}_{data.multi_id}\"\n", 78 | " else:\n", 79 | " name = data.name\n", 80 | " datadict[name] = pd.DataFrame(data.data)\n", 81 | " datadict[name]['timestamp_tiplot'] = datadict[name]['timestamp'] /1e6\n", 82 | " return datadict" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 4, 88 | "id": "92c83b71", 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "datadict = parse_file('/home/hamza/Documents/tiplot/logs/mega.ulg')" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "de29b5ff", 98 | "metadata": {}, 99 | "source": [ 100 | "## Adding logged_messages to datadict" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 5, 106 | "id": "0954836c", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "message_data = []\n", 111 | "for message in ulg.logged_messages:\n", 112 | " message_dict = {}\n", 113 | " message_dict['timestamp'] = message.timestamp\n", 114 | " message_dict['message'] = message.message\n", 115 | " message_dict['timestamp_tiplot'] = message_dict['timestamp'] / 1e6\n", 116 | " message_data.append(message_dict)\n", 117 | "\n", 118 | "datadict['ulg'] = {}\n", 119 | "datadict['ulg'] = pd.DataFrame(message_data)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "af64827c", 125 | "metadata": {}, 126 | "source": [ 127 | "## Entities" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "id": "c22c4db2", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | " uav = {\n", 138 | " \"name\": \"entity\",\n", 139 | " \"alpha\": 1,\n", 140 | " \"useRPY\": False,\n", 141 | " \"useXYZ\": True,\n", 142 | " \"pathColor\": \"#007bff\",\n", 143 | " \"wireframe\": False,\n", 144 | " \"color\": \"#ffffff\",\n", 145 | " \"tracked\": True,\n", 146 | " \"scale\": 0.5,\n", 147 | " \"position\": {\n", 148 | " \"table\": \"vehicle_local_position\",\n", 149 | " \"x\": \"x\",\n", 150 | " \"y\": \"y\",\n", 151 | " \"z\": \"z\"\n", 152 | " },\n", 153 | " \"attitude\": {\n", 154 | " \"table\": \"vehicle_attitude\",\n", 155 | " \"q0\": \"q[0]\",\n", 156 | " \"q1\": \"q[1]\",\n", 157 | " \"q2\": \"q[2]\",\n", 158 | " \"q3\": \"q[3]\"\n", 159 | " }\n", 160 | " }\n", 161 | "\n", 162 | " uav_setpoint = {\n", 163 | " \"name\": \"entity setpoint\",\n", 164 | " \"alpha\": 1,\n", 165 | " \"useRPY\": False,\n", 166 | " \"useXYZ\": True,\n", 167 | " \"pathColor\": \"#007bff\",\n", 168 | " \"wireframe\": False,\n", 169 | " \"color\": \"#ffffff\",\n", 170 | " \"tracked\": True,\n", 171 | " \"scale\": 0.5,\n", 172 | " \"position\": {\n", 173 | " \"table\": \"vehicle_local_position_setpoint\",\n", 174 | " \"x\": \"x\",\n", 175 | " \"y\": \"y\",\n", 176 | " \"z\": \"z\"\n", 177 | " },\n", 178 | " \"attitude\": {\n", 179 | " \"table\": \"vehicle_attitude\",\n", 180 | " \"q0\": \"q[0]\",\n", 181 | " \"q1\": \"q[1]\",\n", 182 | " \"q2\": \"q[2]\",\n", 183 | " \"q3\": \"q[3]\"\n", 184 | " }\n", 185 | " }" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "id": "42d40008", 191 | "metadata": {}, 192 | "source": [ 193 | "## Sending data over the socket" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 7, 199 | "id": "ba598ceb", 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "send_zipped_pickle(socket, [datadict, [uav, uav_setpoint]])" 204 | ] 205 | } 206 | ], 207 | "metadata": { 208 | "kernelspec": { 209 | "display_name": "Python 3 (ipykernel)", 210 | "language": "python", 211 | "name": "python3" 212 | }, 213 | "language_info": { 214 | "codemirror_mode": { 215 | "name": "ipython", 216 | "version": 3 217 | }, 218 | "file_extension": ".py", 219 | "mimetype": "text/x-python", 220 | "name": "python", 221 | "nbconvert_exporter": "python", 222 | "pygments_lexer": "ipython3", 223 | "version": "3.10.6" 224 | } 225 | }, 226 | "nbformat": 4, 227 | "nbformat_minor": 5 228 | } 229 | -------------------------------------------------------------------------------- /templates/photo_geolocations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "7869b4da", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from exif import Image\n", 11 | "import pandas as pd\n", 12 | "import PIL\n", 13 | "import os\n", 14 | "import zmq\n", 15 | "import zlib\n", 16 | "import pyulog\n", 17 | "import pandas as pd\n", 18 | "import numpy as np\n", 19 | "import pickle\n", 20 | "from datetime import datetime" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "id": "1249f5d7", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "port = '5555'\n", 31 | "context = zmq.Context()\n", 32 | "socket = context.socket(zmq.REQ)\n", 33 | "socket.connect(\"tcp://0.0.0.0:%s\" % port)\n", 34 | "\n", 35 | "def send_zipped_pickle(socket, obj, flags=0, protocol=-1):\n", 36 | " p = pickle.dumps(obj, protocol)\n", 37 | " z = zlib.compress(p)\n", 38 | " return socket.send(z, flags=flags)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 3, 44 | "id": "3a05ca7c", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "buff = {\n", 49 | " \"timestamp_tiplot\": [],\n", 50 | " \"timestamp\": [],\n", 51 | " \"lat\": [],\n", 52 | " \"lon\": [],\n", 53 | " \"alt\": [],\n", 54 | " \"zero\": [],\n", 55 | " \"one\": [],\n", 56 | "}\n", 57 | "\n", 58 | "def image_coordinates(image_path):\n", 59 | " global t\n", 60 | " with open(image_path, 'rb') as src:\n", 61 | " img = Image(src)\n", 62 | " if img.has_exif:\n", 63 | " try:\n", 64 | " img.gps_longitude\n", 65 | " coords = (decimal_coords(img.gps_latitude,\n", 66 | " img.gps_latitude_ref),\n", 67 | " decimal_coords(img.gps_longitude,\n", 68 | " img.gps_longitude_ref))\n", 69 | " except AttributeError:\n", 70 | " print('No Coordinates')\n", 71 | " else:\n", 72 | " print('The Image has no EXIF information')\n", 73 | " timestamp = img.datetime_original\n", 74 | " lat = coords[0]\n", 75 | " lon = coords[1]\n", 76 | " alt = img.gps_altitude\n", 77 | " dt = datetime.strptime(timestamp, '%Y:%m:%d %H:%M:%S')\n", 78 | " \n", 79 | " buff['timestamp_tiplot'].append(dt.timestamp()/1e6)\n", 80 | " buff['timestamp'].append(timestamp)\n", 81 | " buff['lat'].append(lat)\n", 82 | " buff['lon'].append(lon)\n", 83 | " buff['alt'].append(alt)\n", 84 | " \n", 85 | " buff['zero'].append(0) # for qx,qy,qz\n", 86 | " buff['one'].append(1) # for qw\n", 87 | " \n", 88 | "def decimal_coords(coords, ref):\n", 89 | " decimal_degrees = coords[0] + coords[1] / 60 + coords[2] / 3600\n", 90 | " if ref == \"S\" or ref == \"W\":\n", 91 | " decimal_degrees = -decimal_degrees\n", 92 | " return decimal_degrees\n", 93 | "\n", 94 | "for f in os.listdir(os.getcwd()+\"/photos/\"):\n", 95 | " filename = os.getcwd()+\"/photos/\"+f\n", 96 | " image_coordinates(filename)\n", 97 | " " 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 4, 103 | "id": "f0ce7dec", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "data=pd.DataFrame.from_dict(buff)\n", 108 | "data = data.sort_values(by=['timestamp_tiplot'])\n", 109 | "datadict = {\"info\": data}\n" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 5, 115 | "id": "bacb17e7", 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "uav = {\n", 120 | " \"name\": \"uav\",\n", 121 | " \"alpha\": 1,\n", 122 | " \"useRPY\": False,\n", 123 | " \"useXYZ\": False,\n", 124 | " \"pathColor\": \"#00ff1e\",\n", 125 | " \"wireframe\": False,\n", 126 | " \"color\": \"#ffffff\",\n", 127 | " \"tracked\": False,\n", 128 | " \"scale\": 1,\n", 129 | " \"position\": {\n", 130 | " \"table\": \"info\",\n", 131 | " \"longitude\": \"lon\",\n", 132 | " \"lattitude\": \"lat\",\n", 133 | " \"altitude\": \"alt\"\n", 134 | " },\n", 135 | " \"attitude\": {\n", 136 | " \"table\": \"info\",\n", 137 | " \"q0\": \"one\",\n", 138 | " \"q1\": \"zero\",\n", 139 | " \"q2\": \"zero\",\n", 140 | " \"q3\": \"zero\"\n", 141 | " }\n", 142 | " }\n" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 6, 148 | "id": "c7081b12", 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "send_zipped_pickle(socket, [datadict, [uav]])" 153 | ] 154 | } 155 | ], 156 | "metadata": { 157 | "kernelspec": { 158 | "display_name": "Python 3 (ipykernel)", 159 | "language": "python", 160 | "name": "python3" 161 | }, 162 | "language_info": { 163 | "codemirror_mode": { 164 | "name": "ipython", 165 | "version": 3 166 | }, 167 | "file_extension": ".py", 168 | "mimetype": "text/x-python", 169 | "name": "python", 170 | "nbconvert_exporter": "python", 171 | "pygments_lexer": "ipython3", 172 | "version": "3.10.6" 173 | } 174 | }, 175 | "nbformat": 4, 176 | "nbformat_minor": 5 177 | } 178 | --------------------------------------------------------------------------------