Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use the converter utility to convert models in studio app #67

Merged
merged 29 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5a7521d
feat: shapefile converter utilty
AdamPospisil Jun 25, 2024
8ef9bc8
Merge branch 'dev' into feature/coordinates_converter
AdamPospisil Jun 25, 2024
d5dcffa
feat: coordinate conversion for geojson files
AdamPospisil Jun 27, 2024
f132ad3
feat: gltf transform support
AdamPospisil Jul 4, 2024
5c29c32
Merge branch 'dev' into feature/coordinates_converter
AdamPospisil Jul 4, 2024
db2c052
feat: updated readme
AdamPospisil Jul 4, 2024
cac7c52
feat: updated readme
AdamPospisil Jul 4, 2024
9e8a0e3
feat: copy extras for meshes and primitives
AdamPospisil Jul 8, 2024
f5eea21
feat: added tests for shapefile and geojson
AdamPospisil Jul 9, 2024
7a15477
feat: gltf tests & testdata
AdamPospisil Jul 9, 2024
b8c1981
perf: sped up gltf conversion
AdamPospisil Jul 10, 2024
f83e725
chore(devcontainer): add node and python as features to ubuntu base d…
SmallhillCZ Jul 11, 2024
2049aef
feat: add convert dialog to models
SmallhillCZ Jul 11, 2024
330dde4
feat: convert the model on demand
SmallhillCZ Jul 11, 2024
fdbf55d
chore(docs,converter): update readme, add docker compose and dockerfi…
SmallhillCZ Jul 12, 2024
6773f40
feat: use zero compression before the used zip library fixes workers …
SmallhillCZ Jul 12, 2024
44cce92
feat: create duplicate model from the converted data
SmallhillCZ Jul 12, 2024
aa28173
feat: add toast notifications for converting
SmallhillCZ Jul 12, 2024
3988582
feat(converter): remove pip install, added as environment requirement
SmallhillCZ Jul 19, 2024
6b9d662
feat(converter): replace 80MB shapefile with 1.7MB shapefile
SmallhillCZ Jul 19, 2024
d482baa
tests(converter): add github workflow
SmallhillCZ Jul 24, 2024
6a982c2
tests(converter): add smaller noproj file
SmallhillCZ Jul 24, 2024
99e068b
tests(converter): fix asserted return file disposition
SmallhillCZ Jul 24, 2024
997f865
refactor: move testdata outside coordinates_comverter to use in studi…
SmallhillCZ Jul 24, 2024
a83ef83
fix(tests): change test environment to node
SmallhillCZ Jul 24, 2024
47aab68
feat: tests for communication with currency converter
SmallhillCZ Jul 24, 2024
8bde5b1
tests(studio): enable running convertModel test without converter ser…
SmallhillCZ Jul 24, 2024
f7e10e3
Merge remote-tracking branch 'origin/dev' into feature/ms-18
SmallhillCZ Jul 26, 2024
76e0748
chore: add .gitattributes to treat testdata files as binary
SmallhillCZ Jul 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata/* binary
31 changes: 31 additions & 0 deletions .github/workflows/converter-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Coordinates Converter Test

on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- "coordinates_converter/**"
- ".github/workflows/converter-test.yml"
workflow_call:

jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.12

- name: Install dependencies
run: |
cd coordinates_converter
pip install -r requirements.txt

- name: Run tests
run: |
cd coordinates_converter/tests
pytest -rA
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
test:
name: Run tests
uses: ./.github/workflows/test.yml
uses: ./.github/workflows/studio-test.yml

build:
name: Build Docker image
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
types: [opened, reopened, synchronize]
paths:
- "studio/**"
- ".github/workflows/test.yml"
- ".github/workflows/studio-test.yml"
workflow_call:

jobs:
Expand Down
6 changes: 6 additions & 0 deletions coordinates_converter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__
*.zip
*.geojson
*.gltf
*.glb
.pytest_cache
41 changes: 41 additions & 0 deletions coordinates_converter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Metacity Studio Coordinates Converter Utility

## Requirements
- Python 3.12
- pip

## How to install

- `pip install -r requirements.txt` to install dependencies

## How to run

- `fastapi dev main.py` to run on localhost

## How to use (Shapefile)

- example curl - shapefile with projection:
- `curl -F "file=@shapefile.zip" -X POST http://localhost:8000/convert_shapefile?crsTarget=EPSG:4326`
- example curl - shapefile without projection:
- `curl -F "file=@shapefile_noproj.zip" -X POST 'http://localhost:8000/convert_shapefile?crsTarget=EPSG:4326&crsSource=EPSG:5514' --output result.zip`
- `file` - zipped shapefile
- `crsTarget` - EPSG code of coordinate system to convert to
- `crsSource` - EPSG code of initial coordinate system (if shapefile doesn't contain projection info)

## How to use (GeoJSON)

- example curl - GeoJSON with projection:
- `curl -F "file=@data.geojson" -X POST http://localhost:8000/convert_geojson?crsTarget=EPSG:4326`
- example curl - GeoJSON without projection:
- `curl -F "file=@data_noproj.geojson" -X POST 'http://localhost:8000/convert_geojson?crsTarget=EPSG:4326&crsSource=EPSG:5514' --output result.geojson`
- `file` - input geojson
- `crsTarget` - EPSG code of coordinate system to convert to
- `crsSource` - EPSG code of initial coordinate system (if geojson doesn't contain projection info)

## How to use (GLTF)

- example curl - GLTF:
- `curl -F "file=@input.gltf" -X POST 'http://localhost:8000/convert_gltf?crsTarget=EPSG:5514&crsSource=EPSG:4326' --output result.gltf'`
- `file` - input gltf
- `crsTarget` - EPSG code of coordinate system to convert to
- `crsSource` - EPSG code of initial coordinate system
Empty file.
190 changes: 190 additions & 0 deletions coordinates_converter/gltf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import base64
import struct

import numpy as np
import pygltflib
import pyrr
import logging

logger = logging.getLogger("uvicorn.error")


def count_vertices(gltf):
count = 0
for node in gltf.nodes:
if node.mesh is not None:
mesh = gltf.meshes[node.mesh]
for primitive in mesh.primitives:
count += gltf.accessors[primitive.attributes.POSITION].count
return count


def get_vertices(gltf, primitive):
# get the binary data for this mesh primitive from the buffer
accessor = gltf.accessors[primitive.attributes.POSITION]
bufferView = gltf.bufferViews[accessor.bufferView]
buffer = gltf.buffers[bufferView.buffer]
data = gltf.get_data_from_buffer_uri(buffer.uri)

# pull each vertex from the binary buffer and convert it into a tuple of python floats
vertices = []
for i in range(accessor.count):
index = (
bufferView.byteOffset + accessor.byteOffset + i * 12
) # the location in the buffer of this vertex
d = data[index : index + 12] # the vertex data
v = struct.unpack("<fff", d) # convert from base64 to three floats
vertices.append((*v, 1.0)) # add the w component
return np.array(vertices, dtype=np.float32)


def get_triangles(gltf, primitive):
# get the binary data for this mesh primitive from the buffer
accessor = gltf.accessors[primitive.indices]
bufferView = gltf.bufferViews[accessor.bufferView]
buffer = gltf.buffers[bufferView.buffer]
data = gltf.get_data_from_buffer_uri(buffer.uri)

triangles = []
for i in range(accessor.count):
index = (
bufferView.byteOffset + accessor.byteOffset + i * 2
) # the location in the buffer of this vertex
d = data[index : index + 2] # the vertex data
v = struct.unpack("<H", d) # convert from base64 to one uint
triangles.append(v[0])
return np.array(triangles, dtype=np.uint16)


def apply_parent_transform(node, mat, nodes):
for n in nodes:
if n.children and n.children.includes(node):
return get_transformation(n, mat, nodes)
return pyrr.matrix44.create_identity()


def get_transformation(node, mat, nodes):
mat @= apply_parent_transform(node, mat, nodes)

if node.matrix:
mat @= node.matrix
else:
rotation = pyrr.matrix44.create_identity()
translation = pyrr.matrix44.create_identity()
scale = pyrr.matrix44.create_identity()
if node.rotation:
rotation = pyrr.matrix44.create_from_quaternion(node.rotation).transpose()
if node.translation:
translation = pyrr.matrix44.create_from_translation(node.translation)
if node.scale:
scale = pyrr.matrix44.create_from_scale(node.scale)
return mat @ (scale @ rotation @ translation)
return mat


def gltf_transform(gltf_input, transformer=None):
buffer_view_counter = 0
mesh_counter = 0
buffer_offset = 0

gltf_output = pygltflib.GLTF2()
gltf_output.scenes = gltf_input.scenes
gltf_output.scene = gltf_input.scene
gltf_output.nodes = gltf_input.nodes
gltf_output.buffers = [pygltflib.Buffer()]
data = b""
for i, node in enumerate(gltf_input.nodes):
if node.mesh is not None:
mesh_input = gltf_input.meshes[node.mesh]
gltf_output.nodes[i].mesh = mesh_counter
mesh_counter += 1
mat = get_transformation(
node, pyrr.matrix44.create_identity(), gltf_input.nodes
)
primitives_output = []
transform = (
(lambda vertex: transformer.transform(*(vertex @ mat)[:3]))
if transformer
else (lambda vertex: (vertex @ mat)[:3])
)

for primitive in mesh_input.primitives:
vertices = get_vertices(gltf_input, primitive)

vertices_transformed = np.array(
([transform(vertex) for vertex in vertices]),
dtype=np.float32,
)

indices = get_triangles(gltf_input, primitive)

indices_bytes = indices.tobytes()
vertices_bytes = vertices_transformed.tobytes()

primitives_output.append(
pygltflib.Primitive(
attributes=pygltflib.Attributes(
POSITION=buffer_view_counter + 1
),
indices=buffer_view_counter,
extras=primitive.extras,
)
)

gltf_output.accessors.append(
pygltflib.Accessor(
bufferView=buffer_view_counter,
componentType=pygltflib.UNSIGNED_SHORT,
count=indices.size,
type=pygltflib.SCALAR,
max=[int(indices.max())],
min=[int(indices.min())],
)
)
gltf_output.accessors.append(
pygltflib.Accessor(
bufferView=buffer_view_counter + 1,
componentType=pygltflib.FLOAT,
count=vertices_transformed.size // 3,
type=pygltflib.VEC3,
max=vertices_transformed.max(axis=0).tolist(),
min=vertices_transformed.min(axis=0).tolist(),
)
)

gltf_output.bufferViews.append(
pygltflib.BufferView(
buffer=0,
byteOffset=buffer_offset,
byteLength=len(indices_bytes),
target=pygltflib.ELEMENT_ARRAY_BUFFER,
)
)
buffer_offset += len(indices_bytes)
gltf_output.bufferViews.append(
pygltflib.BufferView(
buffer=0,
byteOffset=buffer_offset,
byteLength=len(vertices_bytes),
target=pygltflib.ARRAY_BUFFER,
)
)
buffer_offset += len(vertices_bytes)
data += indices_bytes + vertices_bytes
buffer_view_counter += 2

gltf_output.meshes.append(
pygltflib.Mesh(
name=f"{mesh_input.name}",
primitives=primitives_output,
extras=mesh_input.extras,
)
)
gltf_output.nodes[i].translation = [0, 0, 0]
gltf_output.nodes[i].rotation = [0, 0, 0, 1]
gltf_output.nodes[i].scale = [1, 1, 1]
gltf_output.buffers[0].byteLength = buffer_offset
data = base64.b64encode(data).decode("utf-8")
gltf_output.buffers[0].uri = f"data:application/octet-stream;base64,{data}"
gltf_output.extras = gltf_input.extras # copy over any extras
return gltf_output
Loading
Loading