46 Commits

Author SHA1 Message Date
KatKatKateryna 5ec81ea745 fixes
flake8 / flake8_py3 (push) Has been cancelled
Build / main (3.10) (push) Has been cancelled
Build / admin (3.10) (push) Has been cancelled
Check vulnerabilities / vulnerabilities (push) Has been cancelled
2024-09-06 18:12:09 +01:00
Gergő Jedlicska 79b0116d9a Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 03:20:56 +00:00
KatKatKateryna 9a89b4a7ac colors and materials 2024-09-04 04:20:07 +01:00
Gergő Jedlicska cdc8717bde Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 02:46:07 +00:00
KatKatKateryna 8e643e1e38 img url 2024-09-04 03:45:39 +01:00
Gergő Jedlicska 7141e081cc Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 02:37:36 +00:00
KatKatKateryna 782a392825 error screen; better meshes 2024-09-04 03:37:22 +01:00
Gergő Jedlicska 5ee0e75025 Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 02:11:57 +00:00
KatKatKateryna 3afc8ca5f1 img optimized 2024-09-04 03:11:39 +01:00
Gergő Jedlicska 7bdbbb7cdf Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 02:06:18 +00:00
KatKatKateryna 1679045504 img url 2024-09-04 03:06:01 +01:00
Gergő Jedlicska 3bde1c3b93 Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 01:54:04 +00:00
KatKatKateryna c2a5642faa img width 2024-09-04 02:53:53 +01:00
Gergő Jedlicska 0eb30470c6 Merge branch 'dev' of https://github.com/specklesystems/pygeoapi into dev 2024-09-04 01:37:31 +00:00
KatKatKateryna 8c3202290d Polishing (#8)
* typo

* points and lines in 3d: TODO: edit HTML text, add popups, add pins

* html and popups

* add floating pins

* proper 3d display

* loading screen, fixed mesh displayProperties

* html

* .

* typo
2024-09-04 09:30:48 +08:00
Gergő Jedlicska 652c52937c add start script and reverse proxy config 2024-09-03 08:54:06 +00:00
KatKatKateryna 607dc2e9b4 Revert "timeout header"
This reverts commit c3e415237d.
2024-09-02 12:37:50 +01:00
KatKatKateryna c3e415237d timeout header 2024-09-02 12:36:18 +01:00
KatKatKateryna acca99a891 Revert "gunicorn timeout"
This reverts commit 0a96fda59d.
2024-09-02 12:00:12 +01:00
KatKatKateryna 0a96fda59d gunicorn timeout 2024-09-02 11:54:41 +01:00
KatKatKateryna dbe99613b9 Revert "timeout"
This reverts commit 8ee256648d.
2024-09-02 11:49:32 +01:00
KatKatKateryna 8ee256648d timeout 2024-09-02 11:41:38 +01:00
KatKatKateryna a174694961 Kate speckle poc (#7)
* 3d demos

* url_constructor_demo

* readme update

* troubleshooting

* formatting

* formatting

* readme

* .

* readme

* readme

* fix titles

* colors with opacity

* demo3d

* fix mesh split

* maptalks_fixed geometry

* vertical coords

* 3d display

* building example

* print

* demos
2024-09-02 10:35:27 +08:00
KatKatKateryna d7a5cb6576 invite 2024-08-28 21:15:34 +01:00
KatKatKateryna e8f7d528bd troubleshooting guide 2024-08-28 21:14:23 +01:00
KatKatKateryna 4347cf3bb6 typos; feature limit & message 2024-08-28 20:28:20 +01:00
KatKatKateryna 61da86554d fix feature colors 2024-08-28 03:27:14 +01:00
KatKatKateryna b4f3b45bfd typo 2024-08-28 01:16:59 +01:00
KatKatKateryna 8507fdfb45 add color to properties 2024-08-28 01:14:11 +01:00
KatKatKateryna adbfdf95f5 new tab links 2024-08-27 22:12:06 +01:00
KatKatKateryna 2019d106ad Update README.md 2024-08-28 03:36:24 +08:00
KatKatKateryna 6f1042eb13 numbering 2024-08-27 17:17:15 +01:00
KatKatKateryna 695adb0676 quotes 2024-08-27 16:40:01 +01:00
KatKatKateryna 99fbcd0264 link 2024-08-27 15:21:32 +01:00
KatKatKateryna 0cb2802427 default location 2024-08-27 15:05:44 +01:00
KatKatKateryna b8f9144599 fix sorted FID; logo 2024-08-27 14:54:05 +01:00
KatKatKateryna 46e419e2f3 patch only if not installed 2024-08-27 13:41:09 +01:00
KatKatKateryna 36a3567c2c patching packed 2024-08-27 13:20:38 +01:00
KatKatKateryna 95f603b167 port to 8000 2024-08-27 13:11:56 +01:00
KatKatKateryna 31e35668ba Kate speckle poc (#6)
* Revert "change port"

This reverts commit 92e0b0da9e.

* better map size

* render pretty basemap if API key is available

* page layout

* better error message; all redirects to a new tab

* neat tables

* upd readme

* api key variable

* remove manual zoom setting

* reply on speckle_type for traversal

* receive custom attributes

* fix attributes

* fixes

* fixed deserialization; default color

* assign all props to all elements

* send complete displayValues for Meshes

* refactor

* more refactor

* last fixes

* use 3 dimensions

* support ICurves and new GisFeatures, and (possible) hatches

* fix hatches

* fix orientation function

* fix orientation for mesh, brep, hatch, polygon

* patch deserializer

* accept vertical polygons

* prioritize mesh colors, if available

* clean url arguments

* color mesh by 2 pts

* Revert "color mesh by 2 pts"

This reverts commit 0d2c12db1d.

* query comments and attachments

* fix time formatting

* fixed comments

* fix threads

* patch before launch

* separate comments and geometry in displayProperties

* add url parameter for data type

* split features by type

* max zoom for tiles to load

* cors

* rename url

* typo

* leaflet_demo_comments

* url fix

* Demo with masterplan

* openlayers demo, no zoom extents

* openLayers example

* Update README - QGIS WFS

* explicit error for deprecated data

* move around

* Update README.md

* better openLayers demo

* Update README.md

* Update README.md

* Update README.md

* better display colors

* attribution

* no print

* explode meshes. todo: URL param; fix Revit

* some improvements

* preserveAttributes parameter

* fix

* proper comment resource_id

* add map to empty page

* sort by height

* remove print

* page description

* page description complete

* change port

* readme
2024-08-27 20:03:51 +08:00
KatKatKateryna 3f42069d9e Kate speckle poc (#5)
* Revert "change port"

This reverts commit 92e0b0da9e.

* better map size

* render pretty basemap if API key is available

* page layout

* better error message; all redirects to a new tab

* neat tables

* upd readme

* api key variable

* remove manual zoom setting

* reply on speckle_type for traversal

* receive custom attributes

* fix attributes

* fixes

* fixed deserialization; default color

* assign all props to all elements

* send complete displayValues for Meshes

* refactor

* more refactor

* last fixes

* use 3 dimensions

* support ICurves and new GisFeatures, and (possible) hatches

* fix hatches

* fix orientation function

* fix orientation for mesh, brep, hatch, polygon

* patch deserializer

* accept vertical polygons

* prioritize mesh colors, if available

* clean url arguments

* color mesh by 2 pts

* Revert "color mesh by 2 pts"

This reverts commit 0d2c12db1d.

* query comments and attachments

* fix time formatting

* fixed comments

* fix threads

* patch before launch

* separate comments and geometry in displayProperties

* add url parameter for data type

* split features by type

* max zoom for tiles to load

* cors

* rename url

* typo

* leaflet_demo_comments

* url fix

* Demo with masterplan

* openlayers demo, no zoom extents

* openLayers example

* Update README - QGIS WFS

* explicit error for deprecated data

* move around

* Update README.md

* better openLayers demo

* Update README.md

* Update README.md

* Update README.md

* better display colors

* attribution

* no print

* explode meshes. todo: URL param; fix Revit

* some improvements

* preserveAttributes parameter

* fix

* proper comment resource_id

* add map to empty page

* sort by height

* remove print

* page description

* page description complete

* change port
2024-08-27 19:59:29 +08:00
KatKatKateryna ec04220183 Update patch_specklepy.py
move import inside the function, as "alien" imports are not recognized by pygeoapi
2024-08-15 20:33:36 +08:00
Gergő Jedlicska fad6300654 Update patch_specklepy.py (#3)
better path handling for specklepy and pygeoapi
2024-08-15 20:32:12 +08:00
KatKatKateryna 7bc42653d0 Kate speckle poc (#2)
* Revert "change port"

This reverts commit 92e0b0da9e.

* server patch

* enforce patch

* Reapply "change port"

This reverts commit 243994d0da.
2024-08-14 12:13:10 +08:00
KatKatKateryna c96d4f6f30 Kate speckle poc (#1)
* MVP

* upd

* upd

* fixed dynamic loading

* don't overwrite YAML, remember the value instead

* remove default link; URL syntax; TODO: lat vs lon; speckle_data None

* crs centralized

* not implemented methods

* remove case from args

* removing client auth; only receive branch

* enable Queryables page

* normal props

* reprojection in bulk

* speed up reprojections

* add GisFeature

* fix geometry

* fix coords

* fix lines

* all GIS data works

* non-gis data scaling fixed

* fix non-gis pts and curves

* backup displayValue

* remove redirect to individual features

* replace old links

* UI without redirects

* display parameters

* reroute from homepage

* operations from Core; set HostApp

* receive exception

* all not implemented

* zoom

* redirect to speckle from individual features

* layer color placeholders

* render styles; server info

* Model info column

* html styles

* display Missing URL message

* return functionality for WFS

* more display colors

* load landing page for GIS apps only

* display invalid input error

* typo

* print out agent

* patch specklepy on install

* fix import

* warning for zero coords; remove Account from auth

* remove transport account only before receive

* better patch

* get the entire stream

* fix request split

* clear provider data

* list browsers to support

* better UI

* UI

* UI and hyperlinks

* large map
2024-08-14 10:29:10 +08:00
KatKatKateryna 92e0b0da9e change port 2024-08-13 11:11:23 +01:00
46 changed files with 5808 additions and 234 deletions
+551
View File
@@ -0,0 +1,551 @@
components:
parameters:
bbox:
description: Only features that have a geometry that intersects the bounding
box are selected.The bounding box is provided as four or six numbers, depending
on whether the coordinate reference system includes a vertical axis (height
or depth).
explode: false
in: query
name: bbox
required: false
schema:
items:
type: number
maxItems: 6
minItems: 4
type: array
style: form
bbox-crs:
description: Indicates the coordinate reference system for the given bbox coordinates.
explode: false
in: query
name: bbox-crs
required: false
schema:
format: uri
type: string
style: form
bbox-crs-epsg:
description: Indicates the EPSG for the given bbox coordinates.
explode: false
in: query
name: bbox-crs
required: false
schema:
default: 4326
type: integer
style: form
crs:
description: Indicates the coordinate reference system for the results.
explode: false
in: query
name: crs
required: false
schema:
format: uri
type: string
style: form
f:
description: The optional f parameter indicates the output format which the
server shall provide as part of the response document. The default format
is GeoJSON.
explode: false
in: query
name: f
required: false
schema:
default: json
enum:
- json
- html
- jsonld
type: string
style: form
lang:
description: The optional lang parameter instructs the server return a response
in a certain language, if supported. If the language is not among the available
values, the Accept-Language header language will be used if it is supported.
If the header is missing, the default server language is used. Note that providers
may only support a single language (or often no language at all), that can
be different from the server language. Language strings can be written in
a complex (e.g. "fr-CA,fr;q=0.9,en-US;q=0.8,en;q=0.7"), simple (e.g. "de")
or locale-like (e.g. "de-CH" or "fr_BE") fashion.
in: query
name: lang
required: false
schema:
default: en-US
enum:
- en-US
- fr-CA
type: string
offset:
description: The optional offset parameter indicates the index within the result
set from which the server shall begin presenting results in the response document. The
first element has an index of 0 (default).
explode: false
in: query
name: offset
required: false
schema:
default: 0
minimum: 0
type: integer
style: form
resourceId:
description: Configuration resource identifier
in: path
name: resourceId
required: true
schema:
type: string
skipGeometry:
description: This option can be used to skip response geometries for each feature.
explode: false
in: query
name: skipGeometry
required: false
schema:
default: false
type: boolean
style: form
vendorSpecificParameters:
description: Additional "free-form" parameters that are not explicitly defined
in: query
name: vendorSpecificParameters
schema:
additionalProperties: true
type: object
style: form
responses:
'200':
description: successful operation
'204':
description: no content
Queryables:
content:
application/json:
schema:
$ref: '#/components/schemas/queryables'
description: successful queryables operation
Tiles:
content:
application/json:
schema:
$ref: '#/components/schemas/tiles'
description: Retrieves the tiles description for this collection
default:
content:
application/json:
schema:
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/exception.yaml
description: Unexpected error
schemas:
queryable:
properties:
description:
description: a human-readable narrative describing the queryable
type: string
language:
default:
- en
description: the language used for the title and description
type: string
queryable:
description: the token that may be used in a CQL predicate
type: string
title:
description: a human readable title for the queryable
type: string
type:
description: the data type of the queryable
type: string
type-ref:
description: a reference to the formal definition of the type
format: url
type: string
required:
- queryable
- type
type: object
queryables:
properties:
queryables:
items:
$ref: '#/components/schemas/queryable'
type: array
required:
- queryables
type: object
tilematrixsetlink:
properties:
tileMatrixSet:
type: string
tileMatrixSetURI:
type: string
required:
- tileMatrixSet
type: object
tiles:
properties:
links:
items:
$ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/schemas/link
type: array
tileMatrixSetLinks:
items:
$ref: '#/components/schemas/tilematrixsetlink'
type: array
required:
- tileMatrixSetLinks
- links
type: object
info:
contact:
email: you@example.org
name: Organization Name
url: https://pygeoapi.io
description: pygeoapi provides an API to geospatial data
license:
name: CC-BY 4.0 license
url: https://creativecommons.org/licenses/by/4.0/
termsOfService: https://creativecommons.org/licenses/by/4.0/
title: Speckle pygeoapi instance
version: 0.18.dev0
x-keywords:
- geospatial
- data
- api
openapi: 3.0.2
paths:
/:
get:
description: Landing page
operationId: getLandingPage
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: Landing page
tags:
- server
/collections:
get:
description: Collections
operationId: getCollections
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: Collections
tags:
- server
/collections/speckle:
get:
description: Latest version of Speckle Model data
operationId: describeSpeckleCollection
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'404':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: Get Speckle data metadata
tags:
- speckle
/collections/speckle/items:
get:
description: Latest version of Speckle Model data
operationId: getSpeckleFeatures
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
- $ref: '#/components/parameters/bbox'
- $ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/limit
- $ref: '#/components/parameters/crs'
- $ref: '#/components/parameters/bbox-crs'
- description: The properties that should be included for each feature. The
parameter value is a comma-separated list of property names.
explode: false
in: query
name: properties
required: false
schema:
items:
enum: []
type: string
type: array
style: form
- $ref: '#/components/parameters/vendorSpecificParameters'
- $ref: '#/components/parameters/skipGeometry'
- $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml
- $ref: '#/components/parameters/offset'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'404':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: Get Speckle data items
tags:
- speckle
options:
description: Latest version of Speckle Model data
operationId: optionsSpeckleFeatures
responses:
'200':
description: options response
summary: Options for Speckle data items
tags:
- speckle
/collections/speckle/items/{featureId}:
get:
description: Latest version of Speckle Model data
operationId: getSpeckleFeature
parameters:
- $ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId
- $ref: '#/components/parameters/crs'
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'404':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: Get Speckle data item by id
tags:
- speckle
options:
description: Latest version of Speckle Model data
operationId: optionsSpeckleFeature
parameters:
- $ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId
responses:
'200':
description: options response
summary: Options for Speckle data item by id
tags:
- speckle
/conformance:
get:
description: API conformance definition
operationId: getConformanceDeclaration
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
responses:
'200':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
'500':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError
summary: API conformance definition
tags:
- server
/jobs:
get:
description: Retrieve a list of jobs
operationId: getJobs
responses:
'200':
$ref: '#/components/responses/200'
'404':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml
default:
$ref: '#/components/responses/default'
summary: Retrieve jobs list
tags:
- jobs
/jobs/{jobId}:
delete:
description: Cancel / delete job
operationId: deleteJob
parameters:
- &id001
description: job identifier
in: path
name: jobId
required: true
schema:
type: string
responses:
'204':
$ref: '#/components/responses/204'
'404':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml
default:
$ref: '#/components/responses/default'
summary: Cancel / delete job
tags:
- jobs
get:
description: Retrieve job details
operationId: getJob
parameters:
- *id001
- $ref: '#/components/parameters/f'
responses:
'200':
$ref: '#/components/responses/200'
'404':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml
default:
$ref: '#/components/responses/default'
summary: Retrieve job details
tags:
- jobs
/jobs/{jobId}/results:
get:
description: Retrieve job results
operationId: getJobResults
parameters:
- *id001
- $ref: '#/components/parameters/f'
responses:
'200':
$ref: '#/components/responses/200'
'404':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml
default:
$ref: '#/components/responses/default'
summary: Retrieve job results
tags:
- jobs
/openapi:
get:
description: This document
operationId: getOpenapi
parameters:
- $ref: '#/components/parameters/f'
- $ref: '#/components/parameters/lang'
- description: UI to render the OpenAPI document
explode: false
in: query
name: ui
required: false
schema:
default: swagger
enum:
- swagger
- redoc
type: string
style: form
responses:
'200':
$ref: '#/components/responses/200'
'400':
$ref: https://schemas.opengis.net\ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter
default:
$ref: '#/components/responses/default'
summary: This document
tags:
- server
/processes:
get:
description: Processes
operationId: getProcesses
parameters:
- $ref: '#/components/parameters/f'
responses:
'200':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ProcessList.yaml
default:
$ref: '#/components/responses/default'
summary: Processes
tags:
- server
/processes/hello-world:
get:
description: An example process that takes a name as input, and echoes it back
as output. Intended to demonstrate a simple process with a single literal
input.
operationId: describeHello-worldProcess
parameters:
- $ref: '#/components/parameters/f'
responses:
'200':
$ref: '#/components/responses/200'
default:
$ref: '#/components/responses/default'
summary: Get process metadata
tags:
- hello-world
/processes/hello-world/execution:
post:
description: An example process that takes a name as input, and echoes it back
as output. Intended to demonstrate a simple process with a single literal
input.
operationId: executeHello-worldJob
requestBody:
content:
application/json:
example:
inputs:
message: An optional message.
name: World
schema:
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/execute.yaml
description: Mandatory execute request JSON
required: true
responses:
'200':
$ref: '#/components/responses/200'
'201':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ExecuteAsync.yaml
'404':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml
'500':
$ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ServerError.yaml
default:
$ref: '#/components/responses/default'
summary: Process Hello World execution
tags:
- hello-world
servers:
- description: pygeoapi provides an API to geospatial data
url: http://localhost:5000
tags:
- description: pygeoapi provides an API to geospatial data
externalDocs:
description: information
url: https://example.org
name: server
- description: Latest version of Speckle Model data
name: speckle
- name: coverages
- name: edr
- name: records
- name: features
- name: maps
- name: processes
- name: jobs
- name: tiles
- name: stac
+3 -1
View File
@@ -1,3 +1,6 @@
access_log
error_log
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@@ -104,7 +107,6 @@ ENV/
*.openapi.yml *.openapi.yml
# development setup examples # development setup examples
example-config.yml
example-openapi.yml example-openapi.yml
# misc # misc
+3
View File
@@ -0,0 +1,3 @@
geo.speckle.systems {
reverse_proxy localhost:8000
}
+171
View File
@@ -8,3 +8,174 @@
[pygeoapi](https://pygeoapi.io) is a Python server implementation of the [OGC API](https://ogcapi.ogc.org) suite of standards. The project emerged as part of the next generation OGC API efforts in 2018 and provides the capability for organizations to deploy a RESTful OGC API endpoint using OpenAPI, GeoJSON, and HTML. pygeoapi is [open source](https://opensource.org/) and released under an [MIT license](https://github.com/geopython/pygeoapi/blob/master/LICENSE.md). [pygeoapi](https://pygeoapi.io) is a Python server implementation of the [OGC API](https://ogcapi.ogc.org) suite of standards. The project emerged as part of the next generation OGC API efforts in 2018 and provides the capability for organizations to deploy a RESTful OGC API endpoint using OpenAPI, GeoJSON, and HTML. pygeoapi is [open source](https://opensource.org/) and released under an [MIT license](https://github.com/geopython/pygeoapi/blob/master/LICENSE.md).
Please read the docs at [https://docs.pygeoapi.io](https://docs.pygeoapi.io) for more information. Please read the docs at [https://docs.pygeoapi.io](https://docs.pygeoapi.io) for more information.
# Speckle implementation of pygeoapi
## How to use Speckle data through OGC API Features
This is the test deployment of the OGC API server for public Speckle projects. It allows you to share your Speckle model as geospatial data in the format of OGC API Features / Web Feature Service, so it can be natively added to a QGIS, ArcGIS or Civil3D project, or embedded into a web map using Leaflet, OpenLayers or other libraries.
Demo page: https://geo.speckle.systems/
### How to construct a valid URL to get georeferenced Speckle layer
URL should start with 'https://geo.speckle.systems/?' followed by required and optional parameters. Parameters should be separated with '&' symbol. You can use the generated link to access OGC API dataset in your preferred software, as well as explore the data in the browser and share with others.
Use the following URL parameters to construct a link that provides Speckle data with your preferred settings::
- speckleUrl (text), required, should contain path to a specific Model in Speckle Project, e.g. 'https://app.speckle.systems/projects/55a29f3e9d/models/2d497a381d'
- dataType (text), optional, choose from: points, lines, polygons or projectcomments
- limit (positive integer), recommended, as some applications might apply their custom feature limit
- preserveAttributes (string), optional, choose from: true, false. If not set, meshes will be split into separate polygons for better display quality.
- crsAuthid (text), an authority string e.g. 'epsg:4326'. If set, LAT, LON and NORTHDEGREES arguments will be ignored.
- lat (number), in range -90 to 90
- lon (number), in range -180 to 180
- northDegrees (number), in range -180 to 180
If GIS-originated Speckle model is loaded, no location arguments are needed.
Example: [https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828](https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828)
### Troubleshooting
List of possible issues you can experience and solutions to them:
- Page or Map stays blank and Developer Tools Console shows "net::ERR_QUIC_PROTOCOL_ERROR 200 (OK)"
Solution: Try reloading the page. Otherwise, if in Google Chrome, navigate to chrome://flags/#enable-quic and change Experimental QUIC Protocol dropdown to Disabled.
- Model seems to be loaded incomplete
Solution: Check the message "feature count limited to ..." next to the Model name on the top of the page. If the message is present, try increasing the feature limit using "&limit=10000" URL parameter
- Attribute table doesn't have original feature attributes and properties
Solution: Enable the URL parameter "&preserveAttributes=true". It is disabled by default due to the faulty display of the 3-dimentional multiPolygons overlapping themselves in 2d space, when viweving in the browser on 2d map. Enabling this parameter might make the multipolygons appear "transparent" due to self-overlap.
Report any other issues here or on our [Community Forum](https://speckle.community/).
## Add Speckle Feature Layers to web-based maps and desktop apps
### Add Speckle layer in Javascript
Javascript-based mapping libraries can load speckle data as JSON using the following function:
```javascript
async function loadSpeckleData() => {
var speckle_model_url = 'https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons';
const speckle_data = await fetch(speckle_model_url, {
headers: {'Accept': 'application/geo+json'}
}).then(response => response.json());
}
```
Then you can add it to the base map (e.g. using Leaflet and OpenStreetMap basemap tiles). The following example assumes an html div element with id="items-map":
```html
<script>
var map = L.map('items-map').setView([ 45 , -75 ], 5);
map.addLayer(new L.TileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>'
}
));
loadSpeckleData();
async function loadSpeckleData() => {
var speckle_model_url = 'https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons';
const speckle_data = await fetch(speckle_model_url, {
headers: {'Accept': 'application/geo+json'}
}).then(response => response.json());
speckle_layer = L.geoJSON(speckle_data, {
onEachFeature: function (feature, layer) {
layer.setStyle({
fillColor: feature.displayProperties['color'],
color: myFillColor,
fillOpacity: 0.8,
weight: feature.displayProperties['lineWidth'],
radius: feature.displayProperties['radius']
});
}
});
speckle_layer.addTo(map);
map.fitBounds(speckle_layer.getBounds())
};
</script>
```
Check out 'speckle_demos' folder for more Leaflet and OpenLayers implementation.
### Add Speckle WFS layer in QGIS
1. Add new WFS Layer
![image](https://github.com/user-attachments/assets/ea168853-dc97-43bf-b9f2-4d0244addb01)
2. Create New connection, specify the name and URL with mandatory "speckleUrl" parameter pointing to the Speckle Model. Preferably add the URL parameter with the custom feature limit (e.g. '&limit=10000'). Then click Detect, and the WFS Version should display "OGC API Features". Click OK.
![image](https://github.com/user-attachments/assets/8bf9f164-bdb1-455e-8298-f0c1d5dd324d)
3. Connect, select the dataset "Speckle data" and click "Add".
![image](https://github.com/user-attachments/assets/73c97729-f3b3-4192-a4cf-667ba147fc6f)
4. Loading of the data might take a minute, then you will be able to Zoom to layer and check the Attribute table. Done!
![image](https://github.com/user-attachments/assets/0708c64e-b063-4f55-b9f4-e791fc32da95)
### Add Speckle OGC API layer in ArcGIS
1. Add new OGC API Connection
![image](https://github.com/user-attachments/assets/8ae33828-93de-428d-81f8-c6115bf05d72)
2. Add URL, preferably add the URL parameter with the custom feature limit (e.g. '&limit=10000')
![image](https://github.com/user-attachments/assets/37f52ad5-b312-4292-b760-fb8c4091a45f)
3. Find Speckle Pygeoapi server in Catalog, add SpeckleData layer to Map
![image](https://github.com/user-attachments/assets/9e8461c6-a3f3-4d4c-8777-1e17f2a2f528)
### Add Speckle WFS layer in Civil3D
TODO
## Local dev
First launch:
```python
python -m venv pygeoapi_venv
cd pygeoapi_venv
Scripts\activate
cd pygeoapi
git clone https://github.com/specklesystems/pygeoapi
git checkout dev
pip install --upgrade pip
pip install -r requirements.txt
python -m pip install --upgrade specklepy==2.19.6
python -m pip install pydantic==1.10.17
python pygeoapi\provider\speckle_utils\patch\patch_specklepy.py
python setup.py install
set PYGEOAPI_CONFIG=example-config.yml // export
set PYGEOAPI_OPENAPI=example-config.yml // export
set MAPTILER_KEY_LOCAL=your_api_key // export, (if available)
pygeoapi openapi generate $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI
pygeoapi serve
```
Repeated launch:
```python
cd pygeoapi_venv
Scripts\activate
cd pygeoapi
python setup.py install
set PYGEOAPI_CONFIG=example-config.yml
set PYGEOAPI_OPENAPI=example-config.yml
set MAPTILER_KEY_LOCAL=your_api_key
pygeoapi openapi generate $PYGEOAPI_CONFIG > $PYGEOAPI_OPENAPI
pygeoapi serve
```
+131
View File
@@ -0,0 +1,131 @@
# =================================================================
#
# Authors: Tom Kralidis <tomkralidis@gmail.com>
#
# Copyright (c) 2020 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# =================================================================
server:
bind:
host: 0.0.0.0
port: 8000
url: https://geo.speckle.systems
mimetype: application/json; charset=UTF-8
encoding: utf-8
gzip: false
languages:
# First language is the default language
- en-US
- fr-CA
# cors: true
pretty_print: true
limit: 10000
# templates:
# path: /path/to/Jinja2/templates
# static: /path/to/static/folder # css/js/img
map:
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
# manager:
# name: TinyDB
# connection: /tmp/pygeoapi-process-manager.db
# output_dir: /tmp/
# ogc_schemas_location: /opt/schemas.opengis.net
logging:
level: ERROR
#logfile: /tmp/pygeoapi.log
metadata:
identification:
title:
en: Speckle pygeoapi instance
description:
en: pygeoapi provides an API to geospatial data
keywords:
en:
- geospatial
- data
- api
keywords_type: theme
terms_of_service: https://creativecommons.org/licenses/by/4.0/
url: https://example.org
license:
name: CC-BY 4.0 license
url: https://creativecommons.org/licenses/by/4.0/
provider:
name: Organization Name
url: https://pygeoapi.io
contact:
name: Lastname, Firstname
position: Position Title
address: Mailing Address
city: City
stateorprovince: Administrative Area
postalcode: Zip or Postal Code
country: Country
phone: +xx-xxx-xxx-xxxx
fax: +xx-xxx-xxx-xxxx
email: you@example.org
url: https://app.speckle.systems/
hours: Mo-Fr 08:00-17:00
instructions: During hours of service. Off on weekends.
role: pointOfContact
resources:
speckle:
type: collection
title:
en: Speckle data
description:
en: Latest version of Speckle Model data
keywords:
en:
- 3d data
- speckle
links:
- type: text/html
rel: canonical
title: information
href: https://speckle.systems
hreflang: en-US
extents:
spatial:
bbox: [-180,-90,180,90]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 #https://www.opengis.net/def/crs/OGC/1.3/AUTO42001?lat=0&lon=0 # projected CRS
temporal:
begin: 2011-11-11T11:11:11Z
end: null # or empty (either means open ended)
providers:
- type: feature
name: Speckle
data: # some data
id_field: id
title_field: id
hello-world:
type: process
processor:
name: HelloWorld
+74
View File
@@ -125,6 +125,7 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any],
LOGGER.debug('Loading feature provider') LOGGER.debug('Loading feature provider')
p = load_plugin('provider', get_provider_by_type( p = load_plugin('provider', get_provider_by_type(
api.config['resources'][dataset]['providers'], 'feature')) api.config['resources'][dataset]['providers'], 'feature'))
p._load()
except ProviderTypeError: except ProviderTypeError:
try: try:
LOGGER.debug('Loading coverage provider') LOGGER.debug('Loading coverage provider')
@@ -304,7 +305,18 @@ def get_collection_items(
provider_type = 'feature' provider_type = 'feature'
provider_def = get_provider_by_type( provider_def = get_provider_by_type(
collections[dataset]['providers'], provider_type) collections[dataset]['providers'], provider_type)
# clear data if no URL params
load_data = False
for item in request.params:
if item == 'speckleUrl' and len(request.params[item])>40 and ('speckleUrl=' + request.params[item]) in provider_def['data']:
load_data = True
break
if load_data is False:
provider_def['data'] = ""
p = load_plugin('provider', provider_def) p = load_plugin('provider', provider_def)
except ProviderTypeError: except ProviderTypeError:
try: try:
provider_type = 'record' provider_type = 'record'
@@ -555,6 +567,68 @@ def get_collection_items(
content['timeStamp'] = datetime.utcnow().strftime( content['timeStamp'] = datetime.utcnow().strftime(
'%Y-%m-%dT%H:%M:%S.%fZ') '%Y-%m-%dT%H:%M:%S.%fZ')
# Save passed parameters
url_saved_as_data = collections[dataset]['providers'][0]['data']
url_props = []
if isinstance(url_saved_as_data, str):
url_props = url_saved_as_data.lower().split("&")
content['missing_url'] = content['missing_url_href'] = ""
content['requested_data_type'] = "polygons (default)"
content['preserve_attributes'] = "false (default)"
content['speckle_url'] = content['speckle_project_url'] = content['crs_authid'] = content['lat'] = content['lon'] = content['north_degrees'] = content['limit'] = "-"
crsauthid = False
for item in url_props:
# if CRS authid is found, rest will be ignored
if "speckleurl=" in item:
content['speckle_url'] = item.split("speckleurl=")[1]
if content['speckle_url'][-1] == "/":
content['speckle_url'] = content['speckle_url'][:-1]
content['speckle_project_url'] = content['speckle_url'].split("/models")[0]
elif "datatype=" in item:
content['requested_data_type'] = item.split("datatype=")[1]
if content['requested_data_type'] not in ["points", "lines", "polygons", "projectcomments"]:
content['requested_data_type'] = "polygons (default)"
elif "preserveattributes=" in item:
content['preserve_attributes'] = item.split("preserveattributes=")[1]
if content['preserve_attributes'] not in ["true", "false"]:
content['preserve_attributes'] = "false (default)"
elif "crsauthid=" in item:
content['crs_authid'] = item.split("crsauthid=")[1]
crsauthid = True
elif "lat=" in item:
try:
content['lat'] = float(item.split("lat=")[1])
except:
content['lat'] = f"Invalid input, must be numeric: {item.split('lat=')[1]}"
elif "lon=" in item:
try:
content['lon'] = float(item.split("lon=")[1])
except:
content['lon'] = f"Invalid input, must be numeric: {item.split('lon=')[1]}"
elif "northdegrees=" in item:
try:
content['north_degrees'] = float(item.split("northdegrees=")[1])
except:
content['north_degrees'] = f"Invalid input, must be numeric: {item.split('northdegrees=')[1]}"
elif "limit=" in item:
try:
content['limit'] = float(item.split("limit=")[1])
except:
content['limit'] = f"Invalid input, must be integer: {item.split('limit=')[1]}"
if content['speckle_url'] == "-":
content['missing_url'] = "true"
if crsauthid:
content['lat'] += " (not applied)"
content['lon'] += " (not applied)"
content['north_degrees'] += " (not applied)"
if content['limit'] == "-":
content['limit'] = f"{api.config['server']['limit']} (default)"
# Set response language to requested provider locale # Set response language to requested provider locale
# (if it supports language) and/or otherwise the requested pygeoapi # (if it supports language) and/or otherwise the requested pygeoapi
# locale (or fallback default locale) # locale (or fallback default locale)
+65 -12
View File
@@ -29,6 +29,7 @@
# #
# ================================================================= # =================================================================
import copy
import click import click
import json import json
from jsonschema import validate as jsonschema_validate from jsonschema import validate as jsonschema_validate
@@ -36,12 +37,15 @@ import logging
import os import os
import yaml import yaml
from flask import Request
from pygeoapi.util import to_json, yaml_load, THISDIR from pygeoapi.util import to_json, yaml_load, THISDIR
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
CONFIG = {}
def get_config(raw: bool = False) -> dict: def get_config(raw: bool = False, request: Request = None) -> dict:
""" """
Get pygeoapi configurations Get pygeoapi configurations
@@ -50,22 +54,71 @@ def get_config(raw: bool = False) -> dict:
:returns: `dict` of pygeoapi configuration :returns: `dict` of pygeoapi configuration
""" """
if not os.environ.get('PYGEOAPI_CONFIG'): if not os.environ.get("PYGEOAPI_CONFIG"):
raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') raise RuntimeError("PYGEOAPI_CONFIG environment variable not set")
with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: map_api_key_local = os.environ.get("MAPTILER_KEY_LOCAL")
map_api_key_speckle = os.environ.get("MAPTILER_KEY_SPECKLE")
global CONFIG
config_file = os.environ.get("PYGEOAPI_CONFIG")
with open(config_file, encoding="utf8") as fh:
if raw: if raw:
CONFIG = yaml.safe_load(fh) config_yaml = yaml.safe_load(fh)
else: else:
CONFIG = yaml_load(fh) config_yaml = yaml_load(fh)
# assign valid dictionnaries to Speckle resources
speckle_collection_received = copy.deepcopy(config_yaml["resources"]["speckle"])
# for the first time only: assign YAML value to CONFIG. Otherwise, don't modify
if CONFIG == {}:
CONFIG = config_yaml
url_valid = False
speckle_url = ""
if request is not None:
url = request.url.split("?")[-1]
if "projects" in url and "models" in url:
url_valid = True
speckle_url = url
# if a key found, replace basemap URL to MapTiler
# make sure to restrict the usage for the key
if ".speckle.systems" in request.url.split("?")[0] and map_api_key_speckle and len(map_api_key_speckle)>=20:
CONFIG["server"]["map"]["url"] = r'https://api.maptiler.com/maps/dataviz/{z}/{x}/{y}.png' + f'?key={map_api_key_speckle}'
CONFIG["server"]["map"]["key"] = f'{map_api_key_speckle}'
CONFIG["server"]["map"]["attribution"] = r'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>'
elif map_api_key_local and len(map_api_key_local)>=20:
CONFIG["server"]["map"]["url"] = r'https://api.maptiler.com/maps/dataviz/{z}/{x}/{y}.png' + f'?key={map_api_key_local}'
CONFIG["server"]["map"]["key"] = f'{map_api_key_local}'
CONFIG["server"]["map"]["attribution"] = r'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>'
else:
CONFIG["server"]["map"]["url"] = r'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
CONFIG["server"]["map"]["key"] = ""
CONFIG["server"]["map"]["attribution"] = r'&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
# once Speckle URL is found, set it as a provider
if url_valid:
# speckle_collection_pts["title"]["en"] = "Some Points"
# assign speckle url and get the data
speckle_collection_received["providers"][0]["data"] = speckle_url
CONFIG["resources"] = {
"speckle": speckle_collection_received,
}
return CONFIG return CONFIG
def load_schema() -> dict: def load_schema() -> dict:
""" Reads the JSON schema YAML file. """ """Reads the JSON schema YAML file."""
schema_file = THISDIR / 'schemas' / 'config' / 'pygeoapi-config-0.x.yml' schema_file = THISDIR / "schemas" / "config" / "pygeoapi-config-0.x.yml"
with schema_file.open() as fh2: with schema_file.open() as fh2:
return yaml_load(fh2) return yaml_load(fh2)
@@ -93,18 +146,18 @@ def config():
@click.command() @click.command()
@click.pass_context @click.pass_context
@click.option('--config', '-c', 'config_file', help='configuration file') @click.option("--config", "-c", "config_file", help="configuration file")
def validate(ctx, config_file): def validate(ctx, config_file):
"""Validate configuration""" """Validate configuration"""
if config_file is None: if config_file is None:
raise click.ClickException('--config/-c required') raise click.ClickException("--config/-c required")
with open(config_file) as ff: with open(config_file) as ff:
click.echo(f'Validating {config_file}') click.echo(f"Validating {config_file}")
instance = yaml_load(ff) instance = yaml_load(ff)
validate_config(instance) validate_config(instance)
click.echo('Valid configuration') click.echo("Valid configuration")
config.add_command(validate) config.add_command(validate)
+84 -3
View File
@@ -35,7 +35,8 @@ from typing import Union
import click import click
from flask import (Flask, Blueprint, make_response, request, from flask import (Flask, Blueprint, make_response, request,
send_from_directory, Response, Request) send_from_directory, Response, Request, stream_with_context)
from http import HTTPStatus
from pygeoapi.api import API, APIRequest, apply_gzip from pygeoapi.api import API, APIRequest, apply_gzip
import pygeoapi.api.coverages as coverages_api import pygeoapi.api.coverages as coverages_api
@@ -47,7 +48,7 @@ import pygeoapi.api.stac as stac_api
import pygeoapi.api.tiles as tiles_api import pygeoapi.api.tiles as tiles_api
from pygeoapi.openapi import load_openapi_document from pygeoapi.openapi import load_openapi_document
from pygeoapi.config import get_config from pygeoapi.config import get_config
from pygeoapi.util import get_mimetype, get_api_rules from pygeoapi.util import get_mimetype, get_api_rules, render_j2_template
CONFIG = get_config() CONFIG = get_config()
@@ -150,6 +151,9 @@ def execute_from_flask(api_function, request: Request, *args,
:returns: A Response instance :returns: A Response instance
""" """
CONFIG = get_config(request=request)
api_ = API(CONFIG, OPENAPI)
api_request = APIRequest.from_flask(request, api_.locales) api_request = APIRequest.from_flask(request, api_.locales)
content: Union[str, bytes] content: Union[str, bytes]
@@ -171,8 +175,62 @@ def landing_page():
:returns: HTTP response :returns: HTTP response
""" """
collection_id = "speckle"
agent = request.headers.get('User-Agent')
# Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
# Mozilla/5.0 QGIS/32815/Windows 10 Version 2009
# ArcGIS Pro 3.3.0 (00000000000) - ArcGISPro
browser_agent = False
browser_list = ["Chrome", "Safari", "Firefox", "Edg/", "Trident/"]
print(agent)
if "YaBrowser/" in agent:
raise ValueError("Your browser is not supported.")
for br in browser_list:
if br in agent:
browser_agent = True
break
# if requested from the browser, return this, otherwise ignore IF statement
if request.method == 'GET' and browser_agent: # list items
def generate():
yield loading_screen().data
try:
browser_response = execute_from_flask(itemtypes_api.get_collection_items,
request, collection_id,
skip_valid_check=True)
yield browser_response.data
except Exception as ex:
yield error_screen(ex).data
return Response(stream_with_context(generate()))
return get_response(api_.landing_page(request)) return get_response(api_.landing_page(request))
def error_screen(ex: Exception):
"""
Loading empty page
:returns: HTTP response
"""
content = render_j2_template(api_.tpl_config, 'error_screen.html',{"exception": ex})
return get_response((request.headers, HTTPStatus.OK, content))
def loading_screen():
"""
Loading empty page
:returns: HTTP response
"""
content = render_j2_template(api_.tpl_config, 'loading_screen.html',{'url': CONFIG["server"]["url"]})
return get_response((request.headers, HTTPStatus.OK, content))
@BLUEPRINT.route('/openapi') @BLUEPRINT.route('/openapi')
def openapi(): def openapi():
@@ -182,6 +240,7 @@ def openapi():
:returns: HTTP response :returns: HTTP response
""" """
# raise NotImplementedError()
return get_response(api_.openapi_(request)) return get_response(api_.openapi_(request))
@@ -193,6 +252,7 @@ def conformance():
:returns: HTTP response :returns: HTTP response
""" """
# raise NotImplementedError()
return get_response(api_.conformance(request)) return get_response(api_.conformance(request))
@@ -206,6 +266,7 @@ def get_tilematrix_set(tileMatrixSetId=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(tiles_api.tilematrixset, request, return execute_from_flask(tiles_api.tilematrixset, request,
tileMatrixSetId) tileMatrixSetId)
@@ -218,6 +279,7 @@ def get_tilematrix_sets():
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(tiles_api.tilematrixsets, request) return execute_from_flask(tiles_api.tilematrixsets, request)
@@ -232,6 +294,7 @@ def collections(collection_id=None):
:returns: HTTP response :returns: HTTP response
""" """
# raise NotImplementedError()
return get_response(api_.describe_collections(request, collection_id)) return get_response(api_.describe_collections(request, collection_id))
@@ -245,6 +308,7 @@ def collection_schema(collection_id):
:returns: HTTP response :returns: HTTP response
""" """
# raise NotImplementedError()
return get_response(api_.get_collection_schema(request, collection_id)) return get_response(api_.get_collection_schema(request, collection_id))
@@ -258,10 +322,12 @@ def collection_queryables(collection_id=None):
:returns: HTTP response :returns: HTTP response
""" """
# raise NotImplementedError()
return execute_from_flask(itemtypes_api.get_collection_queryables, request, return execute_from_flask(itemtypes_api.get_collection_queryables, request,
collection_id) collection_id)
# @BLUEPRINT.route('/')
@BLUEPRINT.route('/collections/<path:collection_id>/items', @BLUEPRINT.route('/collections/<path:collection_id>/items',
methods=['GET', 'POST', 'OPTIONS'], methods=['GET', 'POST', 'OPTIONS'],
provide_automatic_options=False) provide_automatic_options=False)
@@ -277,6 +343,7 @@ def collection_items(collection_id, item_id=None):
:returns: HTTP response :returns: HTTP response
""" """
collection_id = 'speckle'
if item_id is None: if item_id is None:
if request.method == 'GET': # list items if request.method == 'GET': # list items
@@ -325,7 +392,7 @@ def collection_coverage(collection_id):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(coverages_api.get_collection_coverage, request, return execute_from_flask(coverages_api.get_collection_coverage, request,
collection_id, skip_valid_check=True) collection_id, skip_valid_check=True)
@@ -340,6 +407,7 @@ def get_collection_tiles(collection_id=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(tiles_api.get_collection_tiles, request, return execute_from_flask(tiles_api.get_collection_tiles, request,
collection_id) collection_id)
@@ -356,6 +424,7 @@ def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(tiles_api.get_collection_tiles_metadata, return execute_from_flask(tiles_api.get_collection_tiles_metadata,
request, collection_id, tileMatrixSetId, request, collection_id, tileMatrixSetId,
skip_valid_check=True) skip_valid_check=True)
@@ -377,6 +446,7 @@ def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None,
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask( return execute_from_flask(
tiles_api.get_collection_tiles_data, tiles_api.get_collection_tiles_data,
request, collection_id, tileMatrixSetId, tileMatrix, tileRow, tileCol, request, collection_id, tileMatrixSetId, tileMatrix, tileRow, tileCol,
@@ -396,6 +466,7 @@ def collection_map(collection_id, style_id=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask( return execute_from_flask(
maps_api.get_collection_map, request, collection_id, style_id maps_api.get_collection_map, request, collection_id, style_id
) )
@@ -412,6 +483,7 @@ def get_processes(process_id=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(processes_api.describe_processes, request, return execute_from_flask(processes_api.describe_processes, request,
process_id) process_id)
@@ -428,6 +500,7 @@ def get_jobs(job_id=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
if job_id is None: if job_id is None:
return execute_from_flask(processes_api.get_jobs, request) return execute_from_flask(processes_api.get_jobs, request)
else: else:
@@ -448,6 +521,7 @@ def execute_process_jobs(process_id):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(processes_api.execute_process, request, return execute_from_flask(processes_api.execute_process, request,
process_id) process_id)
@@ -463,6 +537,7 @@ def get_job_result(job_id=None):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(processes_api.get_job_result, request, job_id) return execute_from_flask(processes_api.get_job_result, request, job_id)
@@ -478,6 +553,7 @@ def get_job_result_resource(job_id, resource):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
# TODO: this does not seem to exist? # TODO: this does not seem to exist?
return get_response(api_.get_job_result_resource( return get_response(api_.get_job_result_resource(
request, job_id, resource)) request, job_id, resource))
@@ -531,6 +607,7 @@ def stac_catalog_root():
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(stac_api.get_stac_root, request) return execute_from_flask(stac_api.get_stac_root, request)
@@ -544,6 +621,7 @@ def stac_catalog_path(path):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
return execute_from_flask(stac_api.get_stac_path, request, path) return execute_from_flask(stac_api.get_stac_path, request, path)
@@ -555,6 +633,7 @@ def admin_config():
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
if request.method == 'GET': if request.method == 'GET':
return get_response(admin_.get_config(request)) return get_response(admin_.get_config(request))
@@ -573,6 +652,7 @@ def admin_config_resources():
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
if request.method == 'GET': if request.method == 'GET':
return get_response(admin_.get_resources(request)) return get_response(admin_.get_resources(request))
@@ -590,6 +670,7 @@ def admin_config_resource(resource_id):
:returns: HTTP response :returns: HTTP response
""" """
raise NotImplementedError()
if request.method == 'GET': if request.method == 'GET':
return get_response(admin_.get_resource(request, resource_id)) return get_response(admin_.get_resource(request, resource_id))
+1
View File
@@ -410,6 +410,7 @@ def set_response_language(headers: dict, *locale_: Locale):
LOGGER.debug(f'Setting Content-Language to {loc_str}') LOGGER.debug(f'Setting Content-Language to {loc_str}')
headers['Content-Language'] = loc_str headers['Content-Language'] = loc_str
headers['Access-Control-Allow-Origin'] = "*"
def add_locale(url, locale_) -> str: def add_locale(url, locale_) -> str:
+1
View File
@@ -60,6 +60,7 @@ PLUGINS = {
'SensorThings': 'pygeoapi.provider.sensorthings.SensorThingsProvider', 'SensorThings': 'pygeoapi.provider.sensorthings.SensorThingsProvider',
'SQLiteGPKG': 'pygeoapi.provider.sqlite.SQLiteGPKGProvider', 'SQLiteGPKG': 'pygeoapi.provider.sqlite.SQLiteGPKGProvider',
'Socrata': 'pygeoapi.provider.socrata.SODAServiceProvider', 'Socrata': 'pygeoapi.provider.socrata.SODAServiceProvider',
'Speckle': 'pygeoapi.provider.speckle.SpeckleProvider',
'TinyDB': 'pygeoapi.provider.tinydb_.TinyDBProvider', 'TinyDB': 'pygeoapi.provider.tinydb_.TinyDBProvider',
'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider', 'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider',
'WMSFacade': 'pygeoapi.provider.wms_facade.WMSFacadeProvider', 'WMSFacade': 'pygeoapi.provider.wms_facade.WMSFacadeProvider',
+476
View File
@@ -0,0 +1,476 @@
# =================================================================
#
# Authors: Matthew Perry <perrygeo@gmail.com>
#
# Copyright (c) 2018 Matthew Perry
# Copyright (c) 2022 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# =================================================================
from datetime import datetime
import json
import logging
import os
import sys
from typing import Any, Dict, List, Optional, Tuple, Union
import uuid
from pygeoapi.provider.base import BaseProvider, ProviderItemNotFoundError
from pygeoapi.util import crs_transform
LOGGER = logging.getLogger(__name__)
HOST_APP = "pygeoapi"
class SpeckleProvider(BaseProvider):
"""Provider class for Speckle server data
This is meant to be simple
(no external services, no dependencies, no schema)
at the expense of performance
(no indexing, full serialization roundtrip on each request)
Not thread safe, a single server process is assumed
This implementation uses the feature 'id' heavily
and will override any 'id' provided in the original data.
The feature 'properties' will be preserved.
TODO:
* query method should take bbox
* instead of methods returning FeatureCollections,
we should be yielding Features and aggregating in the view
* there are strict id semantics; all features in the input GeoJSON file
must be present and be unique strings. Otherwise it will break.
* How to raise errors in the provider implementation such that
* appropriate HTTP responses will be raised
"""
def __init__(self, provider_def):
"""initializer"""
super().__init__(provider_def)
if self.data is None:
self.data = ""
# raise ValueError(
# "Please provide Speckle project link as an argument, e.g.: 'http://localhost:5000/?limit=100000&https://app.speckle.systems/projects/55a29f3e9d/models/2d497a381d'"
# )
from subprocess import run
from pygeoapi.provider.speckle_utils.patch.patch_specklepy import patch_specklepy
try:
import specklepy
except ModuleNotFoundError:
completed_process = run(
[
self.get_python_path(),
"-m",
"pip",
"install",
"--upgrade",
"specklepy==2.19.6",
],
capture_output=True,
)
completed_process = run(
[
self.get_python_path(),
"-m",
"pip",
"install",
"pydantic==1.10.17",
],
capture_output=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} as return code. Full log: {completed_process}"
print(m)
print(completed_process.stdout)
print(completed_process.stderr)
raise Exception(m)
patch_specklepy()
# assign global values
self.url: str = self.data # to store the value and check if self.data has changed
self.speckle_url = self.url.lower().split("speckleurl=")[-1].split("&")[0].split("@")[0].split("?")[0]
self.speckle_data = None
self.model_name = ""
self.crs = None
self.crs_dict = None
self.requested_data_type: str = "polygons (default)" # points, lines, polygons, projectcomments
self.preserve_attributes: str = "false (default)"
self.lat: float = 48.76755913928929 #51.52486388756923
self.lon: float = 11.408741923664028 #0.1621445437168942
self.north_degrees: float = 0
self.extent = [-180,-90,180,90]
self.limit = 10000
self.limit_message = ""
self.material_color_proxies = {}
def get_fields(self):
"""
Get provider field information (names, types)
:returns: dict of fields
"""
fields = {}
LOGGER.debug("Treating all columns as string types")
if self.speckle_data is None:
self._load()
# check if the object was extracted
if isinstance(self.speckle_data, Dict):
if len(self.speckle_data["features"]) == 0:
return fields
for key, value in self.speckle_data["features"][0]["properties"].items():
if isinstance(value, float):
type_ = "number"
elif isinstance(value, int):
type_ = "integer"
else:
type_ = "string"
fields[key] = {"type": type_}
return fields
def _load(self, skip_geometry=None, properties=[], select_properties=[]):
"""Load and validate Speckle data"""
from pygeoapi.provider.speckle_utils.url_utils import get_set_url_parameters
if self.data == "":
return
get_set_url_parameters(self)
# check if it's a new request (self.data was updated and doesn't match self.url)
new_request = False
if self.url != self.data:
new_request = True
self.url = self.data
# check if self.data was updated OR if features were not created yet
if (
new_request is True
or self.speckle_data is None
or (
isinstance(self.speckle_data, dict)
and hasattr(self.speckle_data, "features")
and len(self.speckle_data["features"]) > 0
and not hasattr(self.speckle_data["features"][0], "properties")
)
):
self.speckle_data = self.load_speckle_data()
self.fields = self.get_fields()
# filter by properties if set
if properties:
self.speckle_data["features"] = [
f
for f in self.speckle_data["features"]
if all([str(f["properties"][p[0]]) == str(p[1]) for p in properties])
] # noqa
# All features must have ids, TODO must be unique strings
if isinstance(self.speckle_data, str):
raise Exception(self.speckle_data)
for i in self.speckle_data["features"]:
# for some reason dictionary is changed to list of links
try:
i["properties"]
except:
self.speckle_data = None
return self._load()
if "id" not in i and self.id_field in i["properties"]:
i["id"] = i["properties"][self.id_field]
if skip_geometry:
i["geometry"] = None
if self.properties or select_properties:
i["properties"] = {
k: v
for k, v in i["properties"].items()
if k in set(self.properties) | set(select_properties)
} # noqa
return self.speckle_data
@crs_transform
def query(
self,
offset=0,
limit=10,
resulttype="results",
bbox=[],
datetime_=None,
properties=[],
sortby=[],
select_properties=[],
skip_geometry=False,
q=None,
**kwargs,
):
"""
query the provider
:param offset: starting record to return (default 0)
:param limit: number of records to return (default 10)
:param resulttype: return results or hit limit (default results)
:param bbox: bounding box [minx,miny,maxx,maxy]
:param datetime_: temporal (datestamp or extent)
:param properties: list of tuples (name, value)
:param sortby: list of dicts (property, order)
:param select_properties: list of property names
:param skip_geometry: bool of whether to skip geometry (default False)
:param q: full-text search term(s)
:returns: FeatureCollection dict of 0..n GeoJSON features
"""
# TODO filter by bbox without resorting to third-party libs
data = self._load(
skip_geometry=skip_geometry,
properties=properties,
select_properties=select_properties,
)
if data is None:
return {"features":[], "comments":[], "extent": [-180,-90,180,90]}
data["numberMatched"] = len(data["features"])
if resulttype == "hits":
data["features"] = []
data["comments"] = []
data["extent"] = [-180,-90,180,90]
else:
data["features"] = data["features"][offset : offset + limit]
data["numberReturned"] = len(data["features"])
return data
@crs_transform
def get(self, identifier, **kwargs):
"""
query the provider by id
:param identifier: feature id
:returns: dict of single GeoJSON feature
"""
all_data = self._load()
# if matches
for feature in all_data["features"]:
if str(feature.get("id")) == identifier:
return feature
# default, no match
err = f"item {identifier} not found"
LOGGER.error(err)
raise ProviderItemNotFoundError(err)
def create(self, new_feature):
"""Create a new feature
:param new_feature: new GeoJSON feature dictionary
"""
raise NotImplementedError("Creating features is not supported")
def update(self, identifier, new_feature):
"""Updates an existing feature id with new_feature
:param identifier: feature id
:param new_feature: new GeoJSON feature dictionary
"""
raise NotImplementedError("Updating features is not supported")
def delete(self, identifier):
"""Deletes an existing feature
:param identifier: feature id
"""
raise NotImplementedError("Deleting features is not supported")
def __repr__(self):
return f"<SpeckleProvider> {self.data}"
def load_speckle_data(self: str) -> Dict:
"""Receive and process Speckle data, return geojson."""
from pygeoapi.provider.speckle_utils.server_utils import get_stream_branch, get_client, get_comments
from specklepy.objects.base import Base
from specklepy.logging.exceptions import SpeckleException
from specklepy.api import operations
from specklepy.core.api.wrapper import StreamWrapper
from specklepy.core.api.client import SpeckleClient
from specklepy.logging.metrics import set_host_app
from specklepy.transports.server import ServerTransport
set_host_app(HOST_APP, "0.0.99")
# get URL that will not trigget Client init
url_proj: str = self.speckle_url.split("models")[0]
wrapper: StreamWrapper = StreamWrapper(url_proj)
# set actual branch
wrapper.model_id = self.speckle_url.split("models/")[1].split(" ")[0].split("/")[0].split("&")[0].split(",")[0].split(";")[0].split("@")[0]
# get stream and branch data
client = get_client(wrapper, url_proj)
stream, branch = get_stream_branch(self, client, wrapper)
if self.requested_data_type == "projectcomments":
comments = get_comments(client, wrapper.stream_id, wrapper.model_id)
# commit_obj = Base() # still need to receive object to get the CRS
else:
comments = {}
# set the Model name
self.model_name = branch['name']
commit = branch["commits"]["items"][0]
objId = commit["referencedObject"]
transport = ServerTransport(client=client, account=client.account, stream_id=wrapper.stream_id)
if transport == None:
raise SpeckleException("Transport not found")
# receive commit
try:
commit_obj = operations.receive(objId, transport, None)
except Exception as ex:
# e.g. SpeckleException: Can't get object b53a53697a/f8ce82b242e05eeaab4c6c59fb25e4a0: HTTP error 404 ()
raise ex
client.commit.received(
wrapper.stream_id,
commit["id"],
source_application="pygeoapi",
message="Received commit in pygeoapi",
)
print(f"Rendering model '{branch['name']}' of the project '{stream['name']}'")
speckle_data = self.traverse_data(commit_obj, comments)
speckle_data["features"].extend(speckle_data["comments"])
speckle_data["comments"] = []
speckle_data["project"] = stream['name']
speckle_data["model"] = branch['name']
speckle_data["model_last_version_date"] = datetime.strptime(commit['createdAt'].replace("T", " ").replace("Z","").split(".")[0], '%Y-%m-%d %H:%M:%S')
speckle_data["model_id"] = wrapper.model_id
speckle_data["extent"] = self.extent
speckle_data["limit_message"] = self.limit_message
return speckle_data
def traverse_data(self, commit_obj, comments) -> Dict:
"""Traverse Speckle commit and return geojson with features."""
from specklepy.objects.geometry import Point, Line, Curve, Arc, Circle, Ellipse, Polyline, Polycurve, Mesh, Brep
from specklepy.objects.GIS.layers import VectorLayer
from specklepy.objects.GIS.geometry import GisPolygonElement
from specklepy.objects.GIS.GisFeature import GisFeature
from specklepy.objects.graph_traversal.traversal import (
GraphTraversal,
TraversalRule,
)
from pygeoapi.provider.speckle_utils.crs_utils import get_set_crs_settings
from pygeoapi.provider.speckle_utils.feature_utils import create_features
from pygeoapi.provider.speckle_utils.display_utils import set_default_color, get_material_color_proxies
supported_classes = [GisFeature, GisPolygonElement, Mesh, Brep, Point, Line, Polyline, Curve, Arc, Circle, Ellipse, Polycurve]
supported_types = [y().speckle_type for y in supported_classes]
supported_types.extend([
"Objects.Other.Revit.RevitInstance",
"Objects.BuiltElements.Revit.RevitWall",
"Objects.BuiltElements.Revit.RevitFloor",
"Objects.BuiltElements.Revit.RevitStair",
"Objects.BuiltElements.Revit.RevitColumn",
"Objects.BuiltElements.Revit.RevitBeam",
"Objects.BuiltElements.Revit.RevitElement",
"Objects.BuiltElements.Revit.RevitRebar"])
# traverse commit
data: Dict[str, Any] = {
"type": "FeatureCollection",
"features": [],
"comments": [],
"extent": [-180,-90,180,90],
"model_crs": "-",
}
rule = TraversalRule(
[lambda _: True],
lambda x: [
item
for item in x.get_member_names()
if isinstance(getattr(x, item, None), list)
and (x.speckle_type.split(":")[-1] not in supported_types or isinstance(x, VectorLayer))
],
)
context_list = [x for x in GraphTraversal([rule]).traverse(commit_obj)]
get_set_crs_settings(self, commit_obj, context_list, data)
set_default_color(context_list)
self.material_color_proxies: dict = get_material_color_proxies(commit_obj)
create_features(self, context_list, comments, data)
# sort features by height
#if len(data['features']) == len(data['heights']):
#feat_array = np.array(data['features'])
#heights_array = np.array(data['heights'])
#inds = heights_array.argsort()
#sorted = feat_array[inds].tolist()
time1 = datetime.now()
sorted_list = sorted(data['features'], key=lambda d: d['max_height'])
for i, _ in enumerate(sorted_list):
sorted_list[i]["properties"]["FID"] = i+1
data['features'] = sorted_list
time2 = datetime.now()
print(f"Sorting time: {(time2-time1).total_seconds()}")
return data
def get_python_path(self) -> str:
"""Get current Python executable path."""
if sys.platform.startswith("linux"):
return sys.executable
pythonExec = os.path.dirname(sys.executable)
if sys.platform == "win32":
pythonExec += "\\python"
else:
pythonExec += "/bin/python3"
return pythonExec
@@ -0,0 +1,461 @@
import math
from typing import Dict, List, Tuple
def convert_point(f_base: "Point", coords, coord_counts):
"""Convert Point."""
coords.append([f_base.x, f_base.y, f_base.z])
coord_counts.append([1])
def convert_line(f_base: "Line", coords, coord_counts):
"""Convert Line."""
start = [f_base.start.x, f_base.start.y, f_base.start.z]
end = [f_base.end.x, f_base.end.y, f_base.end.z]
coords.extend([start, end])
coord_counts.append([2])
def convert_polyline(f_base: "Polyline", coords, coord_counts):
"""Convert Polyline."""
coord_counts.append([])
local_coords = [] # to keep track of just the current polyline
local_poly_count = 0
for pt in f_base.as_points():
coords.append([pt.x, pt.y, pt.z])
local_coords.append([pt.x, pt.y, pt.z])
local_poly_count += 1
# closing point
if local_poly_count>2 and f_base.closed is True and local_coords[0] != local_coords[-1]:
coords.append(local_coords[0])
local_poly_count += 1
coord_counts[-1].append(local_poly_count)
def convert_arc(f_base: "Arc", coords, coord_counts):
"""Convert Arc."""
if f_base.plane is None or f_base.plane.normal.z == 0:
normal = 1
else:
normal = f_base.plane.normal.z
# calculate angles and interval
interval, angle1, angle2 = getArcRadianAngle(f_base)
if (angle1 > angle2 and normal == -1) or (angle2 > angle1 and normal == 1):
pass
if angle1 > angle2 and normal == 1:
interval = abs((2 * math.pi - angle1) + angle2)
if angle2 > angle1 and normal == -1:
interval = abs((2 * math.pi - angle2) + angle1)
# set a (random) point density: 24 per 1 rad
pointsNum = math.floor(abs(interval)) * 24
if pointsNum < 4:
pointsNum = 4
# assign coordinates
coord_counts.append([])
local_poly_count = 0
for i in range(0, pointsNum + 1):
k = i / pointsNum # reset values to fraction
angle = angle1 + k * interval * normal
x=f_base.plane.origin.x + f_base.radius * math.cos(angle)
y=f_base.plane.origin.y + f_base.radius * math.sin(angle)
z=f_base.plane.origin.z
coords.append([x, y, z])
local_poly_count += 1
coord_counts[-1].append(local_poly_count)
def convert_circle(f_base: "Circle", coords, coord_counts):
"""Convert Circle."""
if f_base.plane is None or f_base.plane.normal.z == 0:
normal = 1
else:
normal = f_base.plane.normal.z
# set a (random) point density: 24 per 1 rad
interval = 2 * math.pi
pointsNum = math.floor(abs(interval)) * 24
if pointsNum < 4:
pointsNum = 4
# assign coordinates
coord_counts.append([])
local_poly_count = 0
for i in range(0, pointsNum + 1):
k = i / pointsNum # reset values to fraction
angle = k * interval * normal
x=f_base.plane.origin.x + f_base.radius * math.cos(angle)
y=f_base.plane.origin.y + f_base.radius * math.sin(angle)
z=f_base.plane.origin.z
coords.append([x, y, z])
local_poly_count += 1
coord_counts[-1].append(local_poly_count)
def convert_polycurve(f_base: "Polycurve", coords, coord_counts):
"""Convert Polycurve."""
flat_coords = []
flat_coord_count = [0]
# put together results from all segment conversions
for segm in f_base.segments:
convert_icurve(segm, coords, coord_counts)
if len(coord_counts)==0:
continue
flat_coords.extend(coords)
flat_coord_count[-1] += coord_counts[-1][-1]
coords = flat_coords
coord_counts = flat_coord_count
def convert_curve(f_base: "Curve", coords, coord_counts):
"""Convert Curve using its Polyline displayValue."""
return convert_polyline(f_base.displayValue, coords, coord_counts)
def convert_icurve(f_base: "Base", coords, coord_counts):
"""Convert any ICurve."""
from specklepy.objects.geometry import Line, Polyline, Arc, Curve, Circle, Polycurve, Mesh, Brep
if isinstance(f_base, Line):
convert_line(f_base, coords, coord_counts)
elif isinstance(f_base, Polyline):
convert_polyline(f_base, coords, coord_counts)
elif isinstance(f_base, Curve):
convert_curve(f_base, coords, coord_counts)
elif isinstance(f_base, Arc):
convert_arc(f_base, coords, coord_counts)
elif isinstance(f_base, Circle):
convert_circle(f_base, coords, coord_counts)
elif isinstance(f_base, Polycurve):
convert_polycurve(f_base, coords, coord_counts)
def convert_mesh_or_brep(f_base: "Base", coords, coord_counts):
"""Convert Mesh object or Mesh derived from Brep display value."""
from specklepy.objects.geometry import Mesh, Brep
faces = []
vertices = []
# get faces and vertices
if isinstance(f_base, Mesh):
faces = f_base.faces
vertices = f_base.vertices
elif isinstance(f_base, Brep):
if f_base.displayValue is None or (
isinstance(f_base.displayValue, list)
and len(f_base.displayValue) == 0
):
geometry = {}
return
elif isinstance(f_base.displayValue, list):
faces = f_base.displayValue[0].faces
vertices = f_base.displayValue[0].vertices
else:
faces = f_base.displayValue.faces
vertices = f_base.displayValue.vertices
# add coordinates
count: int = 0
for i, pt_count in enumerate(faces):
if i != count:
continue
# old encoding
if pt_count == 0:
pt_count = 3
elif pt_count == 1:
pt_count = 4
local_coords_count = [pt_count]
local_coords = []
for vertex_index in faces[count + 1 : count + 1 + pt_count]:
x = vertices[vertex_index * 3]
y = vertices[vertex_index * 3 + 1]
z = vertices[vertex_index * 3 + 2]
local_coords.append([x, y, z])
count += pt_count + 1
valid: bool = fix_polygon_orientation(local_coords, True)
#if valid:
coords.extend(local_coords)
coord_counts.append(local_coords_count)
def convert_polygon(polygon: "Base", coords, coord_counts):
"""Convert GisPolygonGeometry."""
coord_counts.append([])
local_coords_count = 0
local_coords = []
for pt in polygon.boundary.as_points():
local_coords.append([pt.x, pt.y, pt.z])
local_coords_count += 1
valid: bool = fix_polygon_orientation(local_coords, True)
#if valid:
coords.extend(local_coords)
coord_counts[-1].append(local_coords_count)
for void in polygon.voids:
local_coords_count = 0
local_coords = []
for pt_void in void.as_points():
local_coords.append([pt_void.x, pt_void.y, pt_void.z])
local_coords_count += 1
valid: bool = fix_polygon_orientation(local_coords, False)
#if valid:
coords.extend(local_coords)
coord_counts[-1].append(local_coords_count)
def convert_hatch(hatch: "Base", coords, coord_counts):
"""Convert Hatch."""
coord_counts.append([])
loops: list = hatch["loops"]
boundary = None
voids = []
for loop in loops:
if len(loops)==1 or loop["Type"] == 1: # Outer
boundary = loop["Curve"]
else:
voids.append(loop["Curve"])
if boundary is None:
return
# record coordinates
local_coords_count = []
local_coords = []
convert_icurve(boundary, local_coords, local_coords_count)
valid: bool = fix_polygon_orientation(local_coords, True)
#if valid:
coords.extend(local_coords)
coord_counts.extend(local_coords_count)
for void in voids:
local_coords_count = []
local_coords = []
convert_icurve(void, local_coords, local_coords_count)
valid: bool = fix_polygon_orientation(local_coords, False)
#if valid:
coords.extend(local_coords)
coord_counts.extend(local_coords_count)
def assign_geometry(self: "SpeckleProvider", feature: Dict, f_base) -> Tuple[ List[List[List[float]]], List[List[None| List[int]]] ]:
"""Assign geom type and convert object coords into flat lists of coordinates and schema."""
from specklepy.objects.geometry import Base, Point, Line, Polyline, Arc, Curve, Circle, Polycurve, Mesh, Brep
from specklepy.objects.GIS.geometry import GisPolygonGeometry
geometry = feature["geometry"]
coords = []
coord_counts = []
if isinstance(f_base, Base) and f_base.speckle_type.endswith("Feature") and len(f_base["geometry"]) > 0: # isinstance(f_base, GisFeature) and len(f_base.geometry) > 0:
# GisFeature doesn't deserialize properly, need to check for speckle_type
if self.requested_data_type == "points" and isinstance(f_base["geometry"][0], Point):
geometry["type"] = "MultiPoint"
coord_counts.append(None) # as an indicator of a Multi..type
for geom in f_base["geometry"]:
convert_point(geom, coords, coord_counts)
elif self.requested_data_type == "lines" and isinstance(f_base["geometry"][0], Polyline):
geometry["type"] = "MultiLineString"
coord_counts.append(None)
for geom in f_base["geometry"]:
convert_polyline(geom, coords, coord_counts)
elif self.requested_data_type.startswith("polygons") and isinstance(f_base["geometry"][0], GisPolygonGeometry):
geometry["type"] = "MultiPolygon"
coord_counts.append(None)
for geom in f_base["geometry"]:
convert_polygon(geom, coords, coord_counts)
elif self.requested_data_type == "points":
if isinstance(f_base, Point):
geometry["type"] = "MultiPoint"
coord_counts.append(None) # as an indicator of a Multi..type
convert_point(f_base, coords, coord_counts)
elif isinstance(f_base, Base) and f_base.speckle_type.endswith("PointElement"):
raise TypeError(f"Deprecated speckleType {f_base.speckle_type}. Try loading more recent data.")
elif self.requested_data_type == "lines":
if (isinstance(f_base, Line) or
isinstance(f_base, Polyline) or
isinstance(f_base, Curve) or
isinstance(f_base, Arc) or
isinstance(f_base, Circle) or
isinstance(f_base, Polycurve)):
geometry["type"] = "LineString"
convert_icurve(f_base, coords, coord_counts)
elif isinstance(f_base, Base) and f_base.speckle_type.endswith("LineElement"):
raise TypeError(f"Deprecated speckleType {f_base.speckle_type}. Try loading more recent data.")
elif self.requested_data_type.startswith("polygons"):
if isinstance(f_base, Base) and f_base.speckle_type.endswith(".Hatch"):
geometry["type"] = "MultiPolygon"
coord_counts.append(None)
convert_hatch(f_base, coords, coord_counts)
elif isinstance(f_base, Mesh) or isinstance(f_base, Brep):
geometry["type"] = "MultiPolygon"
coord_counts.append(None) # as an indicator of a Multi..type
convert_mesh_or_brep(f_base, coords, coord_counts)
elif isinstance(f_base, Base) and f_base.speckle_type.endswith("PolygonElement"):
raise TypeError(f"Deprecated speckleType {f_base.speckle_type}. Try loading more recent data.")
elif self.requested_data_type == "projectcomments":
if isinstance(f_base, List): # comment position
geometry["type"] = "MultiPoint"
coord_counts.append(None) # as an indicator of a Multi..type
coords.append([f_base[0], f_base[1], f_base[2]])
coord_counts.append([1])
else:
geometry = {}
# print(f"Unsupported geometry type: {f_base.speckle_type}")
return coords, coord_counts
def getArcRadianAngle(arc: "Arc") -> List[float]:
"""Calculate start & end angle, and interval of an Arc."""
interval = None
normal = arc.plane.normal.z
angle1, angle2 = getArcAngles(arc)
if angle1 is None or angle2 is None:
return None
interval = abs(angle2 - angle1)
if (angle1 > angle2 and normal == -1) or (angle2 > angle1 and normal == 1):
pass
if angle1 > angle2 and normal == 1:
interval = abs((2 * math.pi - angle1) + angle2)
if angle2 > angle1 and normal == -1:
interval = abs((2 * math.pi - angle2) + angle1)
return interval, angle1, angle2
def getArcAngles(poly: "Arc") -> Tuple[float | None]:
if poly.startPoint.x == poly.plane.origin.x:
angle1 = math.pi / 2
else:
angle1 = math.atan(
abs(
(poly.startPoint.y - poly.plane.origin.y)
/ (poly.startPoint.x - poly.plane.origin.x)
)
) # between 0 and pi/2
if (
poly.plane.origin.x < poly.startPoint.x
and poly.plane.origin.y > poly.startPoint.y
):
angle1 = 2 * math.pi - angle1
if (
poly.plane.origin.x > poly.startPoint.x
and poly.plane.origin.y > poly.startPoint.y
):
angle1 = math.pi + angle1
if (
poly.plane.origin.x > poly.startPoint.x
and poly.plane.origin.y < poly.startPoint.y
):
angle1 = math.pi - angle1
if poly.endPoint.x == poly.plane.origin.x:
angle2 = math.pi / 2
else:
angle2 = math.atan(
abs(
(poly.endPoint.y - poly.plane.origin.y)
/ (poly.endPoint.x - poly.plane.origin.x)
)
) # between 0 and pi/2
if (
poly.plane.origin.x < poly.endPoint.x
and poly.plane.origin.y > poly.endPoint.y
):
angle2 = 2 * math.pi - angle2
if (
poly.plane.origin.x > poly.endPoint.x
and poly.plane.origin.y > poly.endPoint.y
):
angle2 = math.pi + angle2
if (
poly.plane.origin.x > poly.endPoint.x
and poly.plane.origin.y < poly.endPoint.y
):
angle2 = math.pi - angle2
return angle1, angle2
def fix_polygon_orientation(
polygon_pts: List[List[float]], clockwise: bool = True
) -> bool:
"""Changes orientation to clockwise (or counter-) and returns False if polygon has no footprint."""
max_number_of_points = 1000
coef = int(len(polygon_pts)/max_number_of_points) if len(polygon_pts)>max_number_of_points else 1
sum_orientation = 0
for k, _ in enumerate(polygon_pts):
index = k + 1
if k == len(polygon_pts) - 1:
index = 0
try:
pt = polygon_pts[k * coef]
pt2 = polygon_pts[index * coef]
sum_orientation += (pt2[0] - pt[0]) * (pt2[1] + pt[1]) # if Speckle Points
except IndexError:
break
if clockwise is True and sum_orientation < 0:
polygon_pts.reverse()
elif clockwise is False and sum_orientation > 0:
polygon_pts.reverse()
if sum_orientation ==0:
return False
return True
@@ -0,0 +1,108 @@
import copy
import math
from typing import List
def reproject_bulk(self, all_coords: List[List[List[float]]], all_coord_counts: List[List[None| List[int]]], geometries) -> None:
"""Reproject coordinates and assign to corresponding geometries."""
from datetime import datetime
# reproject all coords
time1 = datetime.now()
flat_coords = reproject_2d_coords_list(self, all_coords)
time2 = datetime.now()
print(f"Reproject time: {(time2-time1).total_seconds()}")
# define type of features
feat_coord_group_is_multi = [True if None in x else False for x in all_coord_counts]
feat_coord_group_counts = [[ y for y in x if y is not None] for x in all_coord_counts]
feat_coord_group_counts_per_part = [[ sum(y) for y in x if y is not None] for x in all_coord_counts]
feat_coord_group_flat_counts: List[int] = [sum([ sum(y) for y in x if y is not None]) for x in all_coord_counts]
feat_coord_groups = [flat_coords[sum(feat_coord_group_flat_counts[:i]):sum(feat_coord_group_flat_counts[:i])+x] for i, x in enumerate(feat_coord_group_flat_counts)]
for i, geometry in enumerate(geometries):
geometry["coordinates"] = []
if feat_coord_group_is_multi[i] is False:
if geometry["type"] == "Point":
geometry["coordinates"].extend(feat_coord_groups[i][0])
else:
geometry["coordinates"].extend(feat_coord_groups[i])
else:
polygon_parts = []
local_coords_count: List[List[int]] = feat_coord_group_counts[i]
local_coords_count_flat: List[int] = feat_coord_group_counts_per_part[i]
local_flat_coords: List[int] = feat_coord_groups[i]
for c, poly_part_count_lists in enumerate(local_coords_count):
poly_part = []
start_index = sum(local_coords_count_flat[:c]) if c!=0 else 0 # all used coords in all parts
for part_count in poly_part_count_lists:
range_coords_indices = range(start_index, start_index + part_count)
if geometry["type"] == "MultiPoint":
poly_part.extend([local_flat_coords[ind] for ind in range_coords_indices])
else:
poly_part.append([local_flat_coords[ind] for ind in range_coords_indices])
start_index += part_count
if geometry["type"] in ["MultiPoint","MultiLineString"] :
polygon_parts.extend(poly_part)
else:
polygon_parts.append(poly_part)
geometry["coordinates"].extend(polygon_parts)
time3 = datetime.now()
print(f"Construct back geometry time: {(time3-time2).total_seconds()}")
def reproject_2d_coords_list(self, coords_in: List[List[float]]) -> List[List[float]]:
"""Return coordinates in a CRS of SpeckleProvider."""
from pyproj import Transformer
from pyproj import CRS
coords_offset = offset_rotate(self, copy.deepcopy(coords_in))
transformer = Transformer.from_crs(
self.crs,
CRS.from_user_input(4326),
always_xy=True,
)
transformed = [[pt[0], pt[1], pt[2]] for pt in transformer.itransform(coords_offset)]
all_x = [x[0] for x in transformed]
all_y = [x[1] for x in transformed]
self.extent = [min(all_x), min(all_y), max(all_x), max(all_y)]
return transformed
def offset_rotate(self, coords_in: List[list]) -> List[List[float]]:
"""Apply offset and rotation to coordinates, according to SpeckleProvider CRS_dict."""
from specklepy.objects.units import get_scale_factor_from_string
scale_factor = 1
if isinstance(self.crs_dict["units_native"], str):
scale_factor = get_scale_factor_from_string(self.crs_dict["units_native"], "m")
final_coords = []
for coord in coords_in:
a = self.crs_dict["rotation"] * math.pi / 180
x2 = coord[0] * math.cos(a) - coord[1] * math.sin(a)
y2 = coord[0] * math.sin(a) + coord[1] * math.cos(a)
final_coords.append(
[
scale_factor * (x2 + self.crs_dict["offset_x"]),
scale_factor * (y2 + self.crs_dict["offset_y"]),
scale_factor * (coord[2]),
]
)
return final_coords
@@ -0,0 +1,101 @@
from typing import Dict, List
def create_crs_from_wkt(self: "SpeckleProvider", wkt: str | None) -> None:
"""Create and assign CRS object from WKT string."""
from pyproj import CRS
self.crs = CRS.from_user_input(wkt)
def create_crs_from_authid(self: "SpeckleProvider", authid: str | None) -> None:
"""Create and assign CRS object from Authority ID."""
from pyproj import CRS
crs_obj = CRS.from_string(authid)
self.crs = crs_obj
def create_crs_default(self: "SpeckleProvider") -> None:
"""Create and assign custom CRS using SpeckleProvider Lat & Lon."""
from pyproj import CRS
wkt = f'PROJCS["SpeckleCRS_latlon_{self.lat}_{self.lon}", GEOGCS["GCS_WGS_1984", DATUM["D_WGS_1984", SPHEROID["WGS_1984", 6378137.0, 298.257223563]], PRIMEM["Greenwich", 0.0], UNIT["Degree", 0.0174532925199433]], PROJECTION["Transverse_Mercator"], PARAMETER["False_Easting", 0.0], PARAMETER["False_Northing", 0.0], PARAMETER["Central_Meridian", {self.lon}], PARAMETER["Scale_Factor", 1.0], PARAMETER["Latitude_Of_Origin", {self.lat}], UNIT["Meter", 1.0]]'
crs_obj = CRS.from_user_input(wkt)
self.crs = crs_obj
def create_crs_dict(self: "SpeckleProvider", offset_x, offset_y, displayUnits: str | None) -> None:
"""Create and assign CRS_dict of SpeckleProvider."""
if self.crs is not None:
self.crs_dict = {
"wkt": self.crs.to_wkt(),
"offset_x": offset_x,
"offset_y": offset_y,
"rotation": self.north_degrees,
"units_native": displayUnits,
"obj": self.crs,
}
def get_set_crs_settings(self: "SpeckleProvider", commit_obj: "Base", context_list: List["TraversalContext"], data: Dict) -> None:
"""Assign CRS object and Dict to SpeckleProvider."""
from pygeoapi.provider.speckle_utils.display_utils import get_display_units
from specklepy.objects.GIS.CRS import CRS
assign_coordinate_system_to_geojson(data)
root_objects = []
try:
root_objects = [commit_obj] + commit_obj.elements
except AttributeError as ex:
pass # old commit structure
# iterate Speckle objects to get CRS, DisplayUnits, offsets, rotation
crs = None
displayUnits = None
offset_x = 0
offset_y = 0
for item in root_objects:
if (
crs is None
and hasattr(item, "crs")
and isinstance(item["crs"], CRS)
):
crs = item["crs"]
displayUnits = crs["units_native"]
offset_x = crs["offset_x"]
offset_y = crs["offset_y"]
self.north_degrees = crs["rotation"]
create_crs_from_wkt(self, crs["wkt"])
if self.crs.to_authority() is not None:
data["model_crs"] = f"{self.crs.to_authority()}, {self.crs.name} "
else:
data["model_crs"] = f"{self.crs.to_proj4()}"
break
# if CRS not found, create default one and get model units for scaling
if self.crs is None:
create_crs_default(self)
if displayUnits is None:
displayUnits = get_display_units(context_list)
create_crs_dict(self, offset_x, offset_y, displayUnits)
def assign_coordinate_system_to_geojson(data: Dict):
crs = {
"crs": {
"type": "name",
"properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"},
}
}
data["crs"] = crs
@@ -0,0 +1,359 @@
from typing import Dict, List, Tuple
DEFAULT_COLOR = (255 << 24) + (150 << 16) + (150 << 8) + 150
def find_list_of_display_obj(obj) -> List[Tuple["Base", "Base"]]:
"""Get displayable object."""
list_of_display_obj_colors: List = []
# find displayValue if available
displayValue = obj
if hasattr(obj, 'displayValue'):
displayValue = getattr(obj, 'displayValue')
elif hasattr(obj, '@displayValue'):
displayValue = getattr(obj, '@displayValue')
# return List of displayValues
if not isinstance(displayValue, List):
displayValue = [displayValue]
# for Features, return original convertible object and a first item from displayValue
if obj.speckle_type.endswith("Feature"):
if len(displayValue)==0:
return ([(obj, obj)])
else:
return([(obj, displayValue[0])])
separated_display_values: List[Tuple] = separate_display_vals(displayValue)
for item, item_original in separated_display_values:
if item is None:
continue
# read displayObj Colors directly from the obj itself, unless its GisFeature or Revit Element: then keep reading from displayValue
if obj.speckle_type.endswith("Feature") or "BuiltElements.Revit" in obj.speckle_type:
displayValForColor = item_original
else:
displayValForColor = obj
list_of_display_obj_colors.append((item, displayValForColor))
return list_of_display_obj_colors
def separate_display_vals(displayValue: List) -> List[Tuple["Base"]]:
"""Return multiple split geometries."""
from specklepy.objects.geometry import Mesh
display_objs = []
for i, item in enumerate(displayValue):
if isinstance(item, Mesh):
count = 0
all_count = len(item.faces)
for _ in item.faces:
if count < all_count:
faces = []
verts = []
colors = []
vert_num = item.faces[count]
faces.append(vert_num)
faces.extend([ x for x in list(range(vert_num))])
for ind in range(vert_num):
face_vert_index = count+1+ind
vert_index = item.faces[face_vert_index]
new_vert = item.vertices[3*vert_index : 3*vert_index + 3]
verts.extend(new_vert)
if isinstance(item.colors, List) and len(item.colors) > vert_index:
color = item.colors[vert_index]
colors.append(color)
count += vert_num+1
if len(colors)>0:
mesh = Mesh.create(faces= faces, vertices=verts, colors=colors)
else:
mesh = Mesh.create(faces= faces, vertices=verts)
display_objs.append((mesh, item))
elif item is not None:
display_objs.append((item, item))
return display_objs
def find_display_obj(obj) -> Tuple["Base", "Base"]:
"""Get displayable object."""
displayValObj = obj
displayValForColor = obj
# find displayValue if available
displayValue = obj
if hasattr(obj, 'displayValue'):
displayValue = getattr(obj, 'displayValue')
elif hasattr(obj, '@displayValue'):
displayValue = getattr(obj, '@displayValue')
# merge to sigle object, if List
if isinstance(displayValue, List):
displayValue = get_single_display_object(displayValue)
# read displayObj Colors directly from the obj itself, unless its GisFeature or Revit Element: then keep reading from displayValue
if not obj.speckle_type.endswith("Feature") and "BuiltElements.Revit" not in obj.speckle_type:
displayValForColor = obj
else:
displayValForColor = displayValue
# return convertible types as is
if is_convertible(obj):
displayValObj = obj
else:
displayValObj = displayValue
return displayValObj, displayValForColor
def is_convertible(obj) -> bool:
"""Check if the object can be converted directly."""
from specklepy.objects.geometry import Base, Point, Line, Arc, Circle, Curve, Polycurve, Mesh, Brep
if ( (isinstance(obj, Base) and obj.speckle_type.endswith("Feature")) or
isinstance(obj, Point) or
isinstance(obj, Line) or
isinstance(obj, Arc) or
isinstance(obj, Circle) or
isinstance(obj, Curve) or
isinstance(obj, Polycurve) or
isinstance(obj, Mesh) or
isinstance(obj, Brep)):
return True
return False
def get_single_display_object(displayValForColor: List) -> "Base":
"""Get a merged Mesh or a first item from displayValue list."""
from specklepy.objects.geometry import Mesh
faces = []
verts = []
colors = []
for i, item in enumerate(displayValForColor):
if isinstance(item, Mesh):
start_vert_count = int(len(verts)/3)
# only add colors if existing and incoming colors are valid (same length as vertices)
if len(colors) == start_vert_count and isinstance(item.colors, List) and len(item.colors)== int(len(item.vertices)/3)>0:
colors.extend(item.colors)
else:
colors = []
verts.extend(item.vertices)
count = 0
for _ in item.faces:
try:
vert_num = item.faces[count]
faces.append(vert_num)
faces.extend([ x+start_vert_count for x in item.faces[count+1 : count+1+vert_num]])
count += vert_num+1
except IndexError:
break
elif item is not None:
displayValForColor = item
mesh = Mesh.create(faces= faces, vertices=verts, colors=colors)
for prop in displayValForColor[0].get_member_names():
if prop not in ["colors", "vertices", "faces"]:
mesh[prop] = getattr(displayValForColor[0], prop)
displayValForColor = mesh
return displayValForColor
def get_display_units(context_list: List["TraversalContext"]) -> None | str:
"""Get units from either of displayable objects."""
from specklepy.objects.geometry import Base
displayUnits = None
for item in context_list:
if hasattr(item.current, "displayValue"):
try:
displayVal = item.current["displayValue"]
except:
displayVal = item.current.displayValue
if isinstance(displayVal, list) and len(displayVal)>0:
displayUnits = displayVal[0].units
break
elif isinstance(displayVal, Base):
displayUnits = item.current.units
break
else:
if item.current.units is not None:
displayUnits = item.current.units
break
return displayUnits
def get_material_color_proxies(root_obj) -> Dict:
"""Get colors and object IDs using ColorProxies and renderMaterialProxies."""
obj_colors = {}
# first, get colors
try:
colorProxies = root_obj["colorProxies"]
if isinstance(colorProxies, List):
for proxy in colorProxies:
color = proxy.value
a, r, g, b = get_r_g_b(color)
color = f'rgba({r},{g},{b},{a})'
for obj in proxy.objects:
obj_colors[obj] = color
except:
pass
# overwrite with materials if available
try:
materialProxies = root_obj["renderMaterialProxies"]
if isinstance(materialProxies, List):
for proxy in materialProxies:
material = proxy.value
color = material['diffuse']
opacity = material['opacity']
a, r, g, b = get_r_g_b(color)
if opacity is not None and isinstance(opacity, float):
a_test = int(255* opacity)
if 0 <= a_test <= 255:
a = a_test
color = f'rgba({r},{g},{b},{a})'
for obj in proxy.objects:
obj_colors[obj] = color
except:
pass
return obj_colors
def set_default_color(context_list: List["TraversalContext"]) -> None:
"""Get and set the default color."""
from specklepy.objects.GIS.layers import VectorLayer
global DEFAULT_COLOR
DEFAULT_COLOR = (255 << 24) + (150 << 16) + (150 << 8) + 150
for item in context_list:
# for GIS-commits, use default blue color
if isinstance(item.current, VectorLayer):
DEFAULT_COLOR = (255 << 24) + (10 << 16) + (132 << 8) + 255
break
def assign_color(self: "SpeckleProvider", obj_display, props) -> None:
"""Get and assign color to feature displayProperties."""
from specklepy.objects.geometry import Base, Mesh, Brep
try:
color = self.material_color_proxies[obj_display.applicationId]
props['color'] = color
return
except:
pass
# initialize Speckle Blue color
color = DEFAULT_COLOR
opacity = None
try:
# prioritize renderMaterials for Meshes & Brep
if isinstance(obj_display, Mesh) or isinstance(obj_display, Brep):
# print(obj_display.get_member_names())
if hasattr(obj_display, 'renderMaterial'):
color = obj_display['renderMaterial']['diffuse']
opacity = obj_display['renderMaterial']['opacity']
elif hasattr(obj_display, '@renderMaterial'):
color = obj_display['@renderMaterial']['diffuse']
opacity = obj_display['@renderMaterial']['opacity']
elif isinstance(obj_display, Mesh) and isinstance(obj_display.colors, List) and len(obj_display.colors)>1:
sameColors = True
color1 = obj_display.colors[0]
for c in obj_display.colors:
if c != color1:
sameColors = False
break
if sameColors is True:
color = color1
elif hasattr(obj_display, 'displayStyle'):
color = obj_display['displayStyle']['color']
elif hasattr(obj_display, '@displayStyle'):
color = obj_display['@displayStyle']['color']
elif hasattr(obj_display, 'displayStyle'):
color = obj_display['displayStyle']['color']
elif hasattr(obj_display, '@displayStyle'):
color = obj_display['@displayStyle']['color']
elif hasattr(obj_display, 'renderMaterial'):
color = obj_display['renderMaterial']['diffuse']
opacity = obj_display['renderMaterial']['opacity']
elif hasattr(obj_display, '@renderMaterial'):
color = obj_display['@renderMaterial']['diffuse']
opacity = obj_display['@renderMaterial']['opacity']
except Exception as e:
print(e)
a, r, g, b = get_r_g_b(color)
if opacity is not None and isinstance(opacity, float):
a_test = int(255* opacity)
if 0 <= a_test <= 255:
a = a_test
# hex_color = '#%02x%02x%02x' % (r, g, b)
props['color'] = f'rgba({r},{g},{b},{a})'
def get_r_g_b(rgb: int) -> Tuple[int, int, int]:
"""Get R, G, B values from int."""
r = g = b = 0
a = 255
try:
a = (rgb & 0xFF000000) >> 24
r = (rgb & 0xFF0000) >> 16
g = (rgb & 0xFF00) >> 8
b = rgb & 0xFF
except Exception as e:
r = g = b = 150
a = 255
return a, r, g, b
def assign_display_properties(self: "SpeckleProvider", feature: Dict, f_base: "Base", obj_display: "Base") -> None:
"""Assign displayProperties to the feature."""
from specklepy.objects.geometry import Mesh, Brep
assign_color(self, obj_display, feature["displayProperties"])
feature["properties"]["color"] = feature["displayProperties"]["color"]
# other properties for rendering
if isinstance(f_base, Mesh) or isinstance(f_base, Brep):
feature["displayProperties"]['lineWidth'] = 0.3
elif "Line" in feature["geometry"]["type"]:
feature["displayProperties"]['lineWidth'] = 3
else:
feature["displayProperties"]['lineWidth'] = 1
# if "Point" in feature["geometry"]["type"]:
try:
feature["displayProperties"]["radius"] = feature["properties"]["weight"]
except:
feature["displayProperties"]["radius"] = 10
@@ -0,0 +1,207 @@
from datetime import datetime
from typing import Dict, List
def initialize_features(self: "SpeckleProvider", all_coords, all_coord_counts, data, context_list, comments: Dict) -> None:
"""Create features with props and displayProps, and assign flat list of coordinates."""
from pygeoapi.provider.speckle_utils.props_utils import assign_props, assign_missing_props
from pygeoapi.provider.speckle_utils.converter_utils import assign_geometry
from pygeoapi.provider.speckle_utils.display_utils import find_display_obj, assign_display_properties, find_list_of_display_obj
print(f"Creating features..")
time1 = datetime.now()
all_props = []
feature_count = 0
if self.requested_data_type != "projectcomments":
for item in context_list:
if feature_count >= self.limit:
self.limit_message = f" (feature count limited to {self.limit})"
break
f_base = item.current
f_id = item.current.id
f_fid = feature_count + 1
# initialize feature
feature: Dict = {
"type": "Feature",
# "bbox": [-180.0, -90.0, 180.0, 90.0],
"geometry": {},
"displayProperties":{
"object_type": "geometry",
},
"properties": {
"id": f_id,
"FID": f_fid,
"speckle_type": item.current.speckle_type.split(":")[-1],
},
}
# feature geometry, props and displayProps
coords = []
coord_counts = []
if "true" in self.preserve_attributes:
obj_display, obj_get_color = find_display_obj(f_base)
try: # don't break the code if 1 feature fails
coords, coord_counts = assign_geometry(self, feature, obj_display)
except TypeError as ex:
raise ex
except Exception as e:
print(e)
pass
if len(coords)!=0:
all_coords.extend(coords)
all_coord_counts.append(coord_counts)
assign_props(f_base, feature["properties"])
# update list of all properties
for prop in feature["properties"]:
if prop not in all_props:
all_props.append(prop)
assign_display_properties(self, feature, f_base, obj_get_color)
feature["max_height"] = max([c[2] for c in coords])
data["features"].append(feature)
feature_count += 1
else:
list_of_display_obj = find_list_of_display_obj(f_base)
for k, vals in enumerate(list_of_display_obj):
obj_display, obj_get_color = vals
f_fid = feature_count + 1
feature_new: Dict = {
"type": "Feature",
# "bbox": [-180.0, -90.0, 180.0, 90.0],
"geometry": {},
"displayProperties":{
"object_type": "geometry",
},
"properties": {
"id": f_id + "_" + str(k),
"FID": f_fid,
"speckle_type": item.current.speckle_type.split(":")[-1],
},
}
coords = []
coord_counts = []
try: # don't break the code if 1 feature fails
coords, coord_counts = assign_geometry(self, feature_new, obj_display)
except TypeError as ex:
raise ex
except Exception as e:
print(e)
pass
if len(coords)!=0:
all_coords.extend(coords)
all_coord_counts.append(coord_counts)
assign_display_properties(self, feature_new, f_base, obj_get_color)
feature_new["max_height"] = max([c[2] for c in coords])
data["features"].append(feature_new)
feature_count +=1
assign_missing_props(data["features"], all_props)
else:
####################### create comment features
for comm_id, comment in comments.items():
if len(data["comments"]) >= self.limit:
self.limit_message = f" (feature count limited to {self.limit})"
break
# initialize comment
feature: Dict = {
"type": "Feature",
"id": comm_id,
"geometry": {},
"displayProperties": {
"object_type": "comment",
},
"properties": {
"messages": [],
"text_html": "",
"resource_id": "",
"all_attachments": []
},
}
coords = []
coord_counts = []
try: # don't break the code if 1 comment fails
coords, coord_counts = assign_geometry(self, feature, comment["position"])
except Exception as e:
print(e)
pass
if len(coords)!=0:
all_coords.extend(coords)
all_coord_counts.append(coord_counts)
assign_comment_data(comment["items"], feature["properties"])
data["comments"].append(feature)
########################
if len(data["features"])==0 and len(data["comments"])==0:
raise ValueError("No supported features found")
time2 = datetime.now()
print(f"Creating features time: {(time2-time1).total_seconds()}")
def assign_comment_data(comments, properties):
"""Create html text to display for the thread."""
for item in comments:
r'''
"author": author_name,
"date": created_date, # e.g. 2024-08-25T13:52:50.562Z
"text": raw_text,
"attachments": [attachments_paths],
"resource_id": string
'''
try:
formatted_time = datetime.strptime(item["date"].replace("T", " ").replace("Z","").split(".")[0], '%Y-%m-%d %H:%M:%S')
except:
formatted_time = item["date"]
properties["messages"].append(f"Author: {item["author"]}, created: {formatted_time}, text: {item["text"]}, attachments: {[img for img in item["attachments"]]}")
try:
properties["resource_id"] = item["resource_id"]
except:
pass # will not be available for replies, only first comment
properties["text_html"] += f"<b>{item["author"]}</b> at {formatted_time}: <br> &emsp; {item["text"]}<br>"
for img in item["attachments"]:
properties["text_html"] += f" <i> &emsp; '{img}'</i> <br>"
properties["all_attachments"].append(img)
properties["text_html"] += "<br>"
#properties["author"] = comment["author"]
#properties["date"] = comment["date"]
#properties["text"] = comment["text"]
#properties["attachments"] = comment["attachments"]
def create_features(self: "SpeckleProvider", context_list: List["TraversalContext"], comments: Dict, data: Dict) -> None:
"""Create features from the list of traversal context."""
from pygeoapi.provider.speckle_utils.coords_utils import reproject_bulk
all_coords = []
all_coord_counts = []
initialize_features(self, all_coords, all_coord_counts, data, context_list, comments)
all_features = data["features"] + data["comments"]
reproject_bulk(self, all_coords, all_coord_counts, [f["geometry"] for f in all_features])
@@ -0,0 +1,79 @@
import os
import sys
from typing import Optional
_user_data_env_var = "SPECKLE_USERDATA_PATH"
_application_name = "Speckle"
def user_application_data_path() -> "Path":
"""Get the platform specific user configuration folder path"""
from pathlib import Path
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception("Cannot get appdata path from environment.")
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception("Failed to initialize user application data path.", ex)
def ensure_folder_exists(base_path: "Path", folder_name: str) -> "Path":
from pathlib import Path
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def _path() -> Optional["Path"]:
from pathlib import Path
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
def connector_installation_path(host_application: str) -> "Path":
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
# print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
def user_speckle_connector_installation_path(host_application: str) -> "Path":
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return ensure_folder_exists(
ensure_folder_exists(
user_speckle_folder_path(), "connector_installations"
),
host_application,
)
def user_speckle_folder_path() -> "Path":
"""Get the folder where the user's Speckle data should be stored."""
return ensure_folder_exists(
user_application_data_path(), _application_name
)
@@ -0,0 +1,13 @@
from typing import List, Optional
from specklepy.objects.base import Base
class GisFeature(
Base, speckle_type="Objects.GIS.GisFeature", detachable={"displayValue"}
):
"""GIS Feature"""
geometry: Optional[List[Base]] = None
attributes: Base
displayValue: Optional[List[Base]] = None
@@ -0,0 +1,443 @@
import hashlib
import re
import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from uuid import uuid4
from warnings import warn
import ujson
# import for serialization
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.objects.base import Base, DataChunk
from specklepy.transports.abstract_transport import AbstractTransport
PRIMITIVES = (int, float, str, bool)
def hash_obj(obj: Any) -> str:
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
def safe_json_loads(obj: str, obj_id=None) -> Any:
try:
return ujson.loads(obj)
except ValueError as err:
import json
warn(
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big"
f" int error - falling back to json. \nError: {err}",
SpeckleWarning,
)
try:
return ujson.loads(obj[:-2])
except:
return json.loads(obj)
class BaseObjectSerializer:
read_transport: AbstractTransport
write_transports: List[AbstractTransport]
detach_lineage: List[bool] # tracks depth and whether or not to detach
lineage: List[str] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]]
closure_table: Dict[str, Dict[str, int]]
deserialized: Dict[
str, Base
] # holds deserialized objects so objects with same id return the same instance
def __init__(
self,
write_transports: Optional[List[AbstractTransport]] = None,
read_transport: Optional[AbstractTransport] = None,
) -> None:
self.write_transports = write_transports or []
self.read_transport = read_transport
self.detach_lineage = []
self.lineage = []
self.family_tree = {}
self.closure_table = {}
self.deserialized = {}
def write_json(self, base: Base):
"""Serializes a given base object into a json string
Arguments:
base {Base} -- the base object to be decomposed and serialized
Returns:
(str, str) -- a tuple containing the object id of the base object and
the serialized object string
"""
obj_id, obj = self.traverse_base(base)
return obj_id, ujson.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict[str, Any]]:
"""Decomposes the given base object and builds a serializable dictionary
Arguments:
base {Base} -- the base object to be decomposed and serialized
Returns:
(str, dict) -- a tuple containing the object id of the base object and
the constructed serializable dictionary
"""
self.__reset_writer()
if self.write_transports:
for wt in self.write_transports:
wt.begin_write()
obj_id, obj = self._traverse_base(base)
if self.write_transports:
for wt in self.write_transports:
wt.end_write()
return obj_id, obj
def _traverse_base(self, base: Base) -> Tuple[str, Dict]:
if not self.detach_lineage:
self.detach_lineage = [True]
self.lineage.append(uuid4().hex)
object_builder = {"id": "", "speckle_type": "Base", "totalChildrenCount": 0}
object_builder.update(speckle_type=base.speckle_type)
obj, props = base, base.get_serializable_attributes()
while props:
prop = props.pop(0)
value = getattr(obj, prop, None)
chunkable = False
detach = False
# skip props marked to be ignored with "__" or "_"
if prop.startswith(("__", "_")):
continue
# don't prepopulate id as this will mess up hashing
if prop == "id":
continue
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = prop.startswith("@") and re.match(
r"^@\((\d*)\)", prop
)
if dynamic_chunk_match:
chunk_size = dynamic_chunk_match.groups()[0]
base._chunkable[prop] = (
int(chunk_size) if chunk_size else base._chunk_size_default
)
chunkable = prop in base._chunkable
detach = bool(
prop.startswith("@") or prop in base._detachable or chunkable
)
# 1. handle None and primitives (ints, floats, strings, and bools)
if value is None or isinstance(value, PRIMITIVES):
object_builder[prop] = value
continue
# NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
if isinstance(value, Enum):
object_builder[prop] = value.value
continue
# 2. handle Base objects
elif isinstance(value, Base):
child_obj = self.traverse_value(value, detach=detach)
if detach and self.write_transports:
ref_id = child_obj["id"]
object_builder[prop] = self.detach_helper(ref_id=ref_id)
else:
object_builder[prop] = child_obj
# 3. handle chunkable props
elif chunkable and self.write_transports:
chunks = []
max_size = base._chunkable[prop]
chunk = DataChunk()
for count, item in enumerate(value):
if count and count % max_size == 0:
chunks.append(chunk)
chunk = DataChunk()
chunk.data.append(item)
chunks.append(chunk)
chunk_refs = []
for c in chunks:
self.detach_lineage.append(detach)
ref_id, _ = self._traverse_base(c)
ref_obj = self.detach_helper(ref_id=ref_id)
chunk_refs.append(ref_obj)
object_builder[prop] = chunk_refs
# 4. handle all other cases
else:
child_obj = self.traverse_value(value, detach)
object_builder[prop] = child_obj
closure = {}
# add closures & children count to the object
detached = self.detach_lineage.pop()
if self.lineage[-1] in self.family_tree:
closure = {
ref: depth - len(self.detach_lineage)
for ref, depth in self.family_tree[self.lineage[-1]].items()
}
object_builder["totalChildrenCount"] = len(closure)
obj_id = hash_obj(object_builder)
object_builder["id"] = obj_id
if closure:
object_builder["__closure"] = self.closure_table[obj_id] = closure
# write detached or root objects to transports
if detached and self.write_transports:
for t in self.write_transports:
t.save_object(id=obj_id, serialized_object=ujson.dumps(object_builder))
del self.lineage[-1]
return obj_id, object_builder
def traverse_value(self, obj: Any, detach: bool = False) -> Any:
"""Decomposes a given object and constructs a serializable object or dictionary
Arguments:
obj {Any} -- the value to decompose
Returns:
Any -- a serializable version of the given object
"""
if obj is None:
return None
if isinstance(obj, PRIMITIVES):
return obj
# NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
if isinstance(obj, Enum):
return obj.value
elif isinstance(obj, (list, tuple, set)):
if not detach:
return [self.traverse_value(o) for o in obj]
detached_list = []
for o in obj:
if isinstance(o, Base):
self.detach_lineage.append(detach)
ref_id, _ = self._traverse_base(o)
detached_list.append(self.detach_helper(ref_id=ref_id))
else:
detached_list.append(self.traverse_value(o, detach))
return detached_list
elif isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, PRIMITIVES) or v is None:
continue
else:
obj[k] = self.traverse_value(v)
return obj
elif isinstance(obj, Base):
self.detach_lineage.append(detach)
_, base_obj = self._traverse_base(obj)
return base_obj
else:
try:
return obj.dict()
except Exception:
warn(
f"Failed to handle {type(obj)} in"
" `BaseObjectSerializer.traverse_value`",
SpeckleWarning,
)
return str(obj)
def detach_helper(self, ref_id: str) -> Dict[str, str]:
"""
Helper to keep track of detached objects and their depth in the family tree
and create reference objects to place in the parent object
Arguments:
ref_id {str} -- the id of the fully traversed object
Returns:
dict -- a reference object to be inserted into the given object's parent
"""
for parent in self.lineage:
if parent not in self.family_tree:
self.family_tree[parent] = {}
if ref_id not in self.family_tree[parent] or self.family_tree[parent][
ref_id
] > len(self.detach_lineage):
self.family_tree[parent][ref_id] = len(self.detach_lineage)
return {
"referencedId": ref_id,
"speckle_type": "reference",
}
def __reset_writer(self) -> None:
"""
Reinitializes the lineage, and other variables that get used during the json
writing process
"""
self.detach_lineage = [True]
self.lineage = []
self.family_tree = {}
self.closure_table = {}
def read_json(self, obj_string: str) -> Base:
"""Recomposes a Base object from the string representation of the object
Arguments:
obj_string {str} -- the string representation of the object
Returns:
Base -- the base object with all it's children attached
"""
if not obj_string:
return None
self.deserialized = {}
obj = safe_json_loads(obj_string)
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
"""Steps through a base object dictionary and recomposes the base object
Arguments:
obj {dict} -- the dictionary representation of the object
Returns:
Base -- the base object with all its children attached
"""
# make sure an obj was passed and create dict if string was somehow passed
if not obj:
return
if isinstance(obj, str):
obj = safe_json_loads(obj)
if "id" in obj and obj["id"] in self.deserialized:
return self.deserialized[obj["id"]]
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
speckle_type = obj.get("speckle_type")
# if speckle type is not in the object definition, it is treated as a dict
if not speckle_type:
return obj
# get the registered type from base register.
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
# get total children count
if "__closure" in obj:
if not self.read_transport:
raise SpeckleException(
message="Cannot resolve reference - no read transport is defined"
)
closure = obj.pop("__closure")
base.totalChildrenCount = len(closure)
for prop, value in obj.items():
# 1. handle primitives (ints, floats, strings, and bools) or None
if isinstance(value, PRIMITIVES) or value is None:
base.__setattr__(prop, value)
continue
# 2. handle referenced child objects
elif "referencedId" in value:
ref_id = value["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id)
if ref_obj_str:
ref_obj = safe_json_loads(ref_obj_str, ref_id)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
else:
warnings.warn(
f"Could not find the referenced child object of id `{ref_id}`"
f" in the given read transport: {self.read_transport.name}",
SpeckleWarning,
)
base.__setattr__(prop, self.handle_value(value))
# 3. handle all other cases (base objects, lists, and dicts)
else:
base.__setattr__(prop, self.handle_value(value))
if "id" in obj:
self.deserialized[obj["id"]] = base
return base
def handle_value(self, obj: Any):
"""Helper for recomposing a base object by handling the dictionary
representation's values
Arguments:
obj {Any} -- a value from the base object dictionary
Returns:
Any -- the handled value (primitive, list, dictionary, or Base)
"""
if not obj:
return obj
if isinstance(obj, PRIMITIVES):
return obj
# lists (regular and chunked)
if isinstance(obj, list):
obj_list = [self.handle_value(o) for o in obj]
if (
hasattr(obj_list[0], "speckle_type")
and "DataChunk" in obj_list[0].speckle_type
):
# handle chunked lists
data = []
for o in obj_list:
data.extend(o.data)
return data
return obj_list
# bases
if isinstance(obj, dict) and "speckle_type" in obj:
return self.recompose_base(obj=obj)
# dictionaries
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, PRIMITIVES):
continue
else:
obj[k] = self.handle_value(v)
return obj
def get_child(self, obj: Dict):
ref_id = obj["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id)
if not ref_obj_str:
warnings.warn(
f"Could not find the referenced child object of id `{ref_id}` in the"
f" given read transport: {self.read_transport.name}",
SpeckleWarning,
)
return obj
return safe_json_loads(ref_obj_str, ref_id)
@@ -0,0 +1,140 @@
import sys
from pathlib import Path
import shutil
import pygeoapi
def get_specklepy_path():
import specklepy
return Path(specklepy.__file__).parent
def get_pygeoapi_path():
return Path(pygeoapi.__file__).parent
def get_credentials_path():
specklepy_path = get_specklepy_path()
credentials_path = Path(specklepy_path, "core", "api", "credentials.py")
return str(credentials_path)
def get_transport_path():
specklepy_path = get_specklepy_path()
credentials_path = Path(specklepy_path, "transports", "server", "server.py")
return str(credentials_path)
def get_transport_path_src():
credentials_path = Path(get_pygeoapi_path(), "provider", "speckle_utils", "patch", "server.py")
return str(credentials_path)
def get_serializer_path():
specklepy_path = get_specklepy_path()
credentials_path = Path(specklepy_path, "serialization", "base_object_serializer.py")
return str(credentials_path)
def get_serializer_path_src():
credentials_path = Path(get_pygeoapi_path(), "provider", "speckle_utils", "patch", "base_object_serializer.py")
return str(credentials_path)
def get_gis_feature_path_src():
credentials_path = Path(get_pygeoapi_path(), "provider", "speckle_utils", "patch", "GisFeature.py")
return str(credentials_path)
def get_gis_feature_path_dst():
specklepy_path = get_specklepy_path()
credentials_path = Path(specklepy_path, "objects", "GIS", "GisFeature.py")
return str(credentials_path)
def patch_credentials():
"""Patches the installer with the correct connector version and specklepy version"""
file_path = get_credentials_path()
with open(file_path, "r") as file:
lines = file.readlines()
new_lines = []
for i, line in enumerate(lines):
if "Account.model_validate_json" in line:
line = line.replace("Account.model_validate_json", "Account.parse_raw")
new_lines.append(line)
file.close()
with open(file_path, "w") as file:
file.writelines(new_lines)
file.close()
def patch_transport():
"""Patches the installer with the correct connector version and specklepy version"""
server_data = get_transport_path_src()
file_path = get_transport_path()
with open(server_data, "r") as file:
lines = file.readlines()
file.close()
with open(file_path, "w") as file:
file.writelines(lines)
file.close()
def patch_serializer():
"""Patches the installer with the correct connector version and specklepy version"""
server_data = get_serializer_path_src()
file_path = get_serializer_path()
with open(server_data, "r") as file:
lines = file.readlines()
file.close()
with open(file_path, "w") as file:
file.writelines(lines)
file.close()
def complete_patch():
"""Patches the installer with the correct connector version and specklepy version"""
# check file 1
file_path = get_transport_path()
with open(file_path, "r") as file:
lines = file.readlines()
file.close()
if len(lines) < 184:
return False
# check file 1
file_path = get_serializer_path()
with open(file_path, "r") as file:
lines = file.readlines()
file.close()
if len(lines) < 443:
return False
return True
def copy_gis_feature():
shutil.copyfile(get_gis_feature_path_src(), get_gis_feature_path_dst())
def patch_specklepy():
#if complete_patch():
# return
patch_credentials()
copy_gis_feature()
patch_transport()
patch_serializer()
if __name__ == "__main__":
patch_specklepy()
@@ -0,0 +1,184 @@
import json
from typing import Dict, List, Optional
from warnings import warn
import requests
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.abstract_transport import AbstractTransport
from .batch_sender import BatchSender
class ServerTransport(AbstractTransport):
"""
The `ServerTransport` is the vehicle through which you transport objects to and
from a Speckle Server. Provide it to `operations.send()` or `operations.receive()`.
The `ServerTransport` can be authenticated two different ways:
1. by providing a `SpeckleClient`
2. by providing an `Account`
3. by providing a `token` and `url`
```py
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
# here's the data you want to send
block = Block(length=2, height=4)
# next create the server transport - this is the vehicle through which
# you will send and receive
transport = ServerTransport(stream_id=new_stream_id, client=client)
# this serialises the block and sends it to the transport
hash = operations.send(base=block, transports=[transport])
# you can now create a commit on your stream with this object
commit_id = client.commit.create(
stream_id=new_stream_id,
obj_id=hash,
message="this is a block I made in speckle-py",
)
```
"""
def __init__(
self,
stream_id: str,
client: Optional[SpeckleClient] = None,
account: Optional[Account] = None,
token: Optional[str] = None,
url: Optional[str] = None,
name: str = "RemoteTransport",
) -> None:
super().__init__()
if client is None and account is None and token is None and url is None:
raise SpeckleException(
"You must provide either a client or a token and url to construct a"
" ServerTransport."
)
self._name = name
self.account = None
self.saved_obj_count = 0
if account:
self.account = account
url = account.serverInfo.url
elif client:
url = client.url
if not client.account.token:
warn(
SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport"
f" for {url}. Receiving from private streams will fail."
)
)
else:
self.account = client.account
else:
self.account = get_account_from_token(token, url)
self.stream_id = stream_id
self.url = url
self.session = requests.Session()
if self.account.token is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@property
def name(self) -> str:
return self._name
def begin_write(self) -> None:
self.saved_obj_count = 0
def end_write(self) -> None:
self._batch_sender.flush()
def save_object(self, id: str, serialized_object: str) -> None:
self._batch_sender.send_object(id, serialized_object)
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
) -> None:
obj_string = source_transport.get_object(id=id)
self.save_object(id=id, serialized_object=obj_string)
def get_object(self, id: str) -> str:
# endpoint = f"{self.url}/objects/{self.stream_id}/{id}/single"
# r = self.session.get(endpoint, stream=True)
# _, obj = next(r.iter_lines().decode("utf-8")).split("\t")
# return obj
raise SpeckleException(
"Getting a single object using `ServerTransport.get_object()` is not"
" implemented. To get an object from the server, please use the"
" `SpeckleClient.object.get()` route",
NotImplementedError(),
)
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
return {id: False for id in id_list}
def copy_object_and_children(
self, id: str, target_transport: AbstractTransport
) -> str:
endpoint = f"{self.url}/objects/{self.stream_id}/{id}/single"
r = self.session.get(endpoint)
r.encoding = "utf-8"
if r.status_code != 200:
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error"
f" {r.status_code} ({r.text[:1000]})"
)
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
closures = root_obj.get("__closure", {})
# Check which children are not already in the target transport
children_ids = list(closures.keys())
children_found_map = target_transport.has_objects(children_ids)
new_children_ids = [
id for id in children_found_map if not children_found_map[id]
]
# Get the new children
endpoint = f"{self.url}/api/getobjects/{self.stream_id}"
r = self.session.post(
endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True
)
r.encoding = "utf-8"
lines = r.iter_lines(decode_unicode=True, delimiter="},{")
# iter through returned objects saving them as we go
target_transport.begin_write()
all_lines = [line for _,line in enumerate(lines)]
for i, line in enumerate(all_lines):
if line:
hash = line.split('"id": "')[1].split('"')[0]
obj = "{" + line + "}"
if i==0:
obj = obj[2:]
elif i==len(all_lines)-1:
obj = obj[:-2]
target_transport.save_object(hash, obj)
target_transport.save_object(id, root_obj_serialized)
target_transport.end_write()
return root_obj_serialized
@@ -0,0 +1,103 @@
from typing import Dict, List
def assign_props(obj: "Base", props: Dict):
"""Assign properties to the feature from Base object."""
from specklepy.objects.geometry import Base
from specklepy.objects.other import RevitParameter
all_prop_names = obj.get_member_names()
dynamic_prop_names = obj.get_dynamic_member_names()
typed_prop_names = obj.get_typed_member_names()
# check if GIS object
if "attributes" in all_prop_names and isinstance(obj["attributes"], Base):
all_prop_names = obj["attributes"].get_dynamic_member_names()
for prop_name in all_prop_names:
value = getattr(obj["attributes"], prop_name)
if (prop_name
in [
"geometry",
"Speckle_ID",
"id",
]
):
pass
else:
if (
isinstance(value, Base)
or isinstance(value, List)
or isinstance(value, Dict)
):
props[prop_name] = str(value)
else:
props[prop_name] = value
return
# if Rhino:
elif "userStrings" in dynamic_prop_names and isinstance(obj["userStrings"], Base):
all_prop_names = obj["userStrings"].get_dynamic_member_names()
for prop_name in all_prop_names:
if prop_name in ["id"]:
continue
value = getattr(obj["userStrings"], prop_name)
if not isinstance(value, str):
props[prop_name] = str(value)
else:
props[prop_name] = value
return
for prop_name in obj.get_dynamic_member_names():
if (
prop_name
in [
"displayValue",
"displayStyle",
"renderMaterial",
"revitLinkedModelPath",
"id",
]
):
pass
else:
value = getattr(obj, prop_name)
if (
isinstance(value, Base)
or isinstance(value, List)
or isinstance(value, Dict)
):
props[prop_name] = str(value)
else:
props[prop_name] = value
# if Revit:
if "parameters" in all_prop_names and isinstance(obj.parameters, Base):
for prop_name in obj.parameters.get_dynamic_member_names():
if prop_name in ["id","revitLinkedModelPath"]:
continue
param = getattr(obj.parameters, prop_name)
if isinstance(param, RevitParameter):
if not isinstance(param.value, str):
props[prop_name] = str(param.value)
else:
props[prop_name] = param.value
# add after dynamic parameters
def assign_missing_props(features: Dict, all_props: List[str]) -> None:
"""Assign NA values to missing properties."""
# assign all props to all features
for feat in features:
for prop in all_props:
if prop not in list(feat["properties"].keys()):
feat["properties"][prop] = "N/A"
@@ -0,0 +1,219 @@
import os
from pathlib import Path
from typing import Dict, List, Tuple
import pygeoapi
def get_stream_branch(self: "SpeckleProvider", client: "SpeckleClient", wrapper: "StreamWrapper") -> Tuple:
"""Get stream and branch from the server."""
from specklepy.logging.exceptions import SpeckleException
branch = None
stream = client.stream.get(
id = wrapper.stream_id, branch_limit=100
)
if isinstance(stream, Exception):
raise SpeckleException(stream.message+ ", "+ self.speckle_url)
for br in stream['branches']['items']:
if br['id'] == wrapper.model_id:
branch = br
break
return stream, branch
def get_client(wrapper: "StreamWrapper", url_proj: str) -> "SpeckleClient":
"""Get unauthenticated SpeckleClient."""
from specklepy.core.api.client import SpeckleClient
# get client by URL, no authentication
client = SpeckleClient(host=wrapper.host, use_ssl=wrapper.host.startswith("https"))
client.account.serverInfo.url = url_proj.split("/projects")[0]
return client
def get_comments(client: "SpeckleClient", project_id: str, model_id: str):
"""Query comments from the Project and Model (if recorded in Comment)."""
from gql import gql
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
# get Project data
query = gql(
"""
query Comments ($project_id: String!) {
project(id: $project_id) {
commentThreads {
totalCount
items{
id
author{
name
}
createdAt
rawText
text{
attachments{
id
fileName
fileType
fileSize
}
}
viewerResources{
modelId
}
viewerState
replies{
items{
id
author{
name
}
createdAt
rawText
text{
attachments{
id
fileName
fileType
fileSize
}
}
viewerResources{
modelId
}
viewerState
}
}
}
}
}
}
"""
)
params = {
"project_id": project_id,
}
response_data = client.httpclient.execute(query, params)
threads = response_data["project"]["commentThreads"]["items"]
threads_objs = {}
for thread in threads:
comment_data = get_info_from_comment(thread, project_id, model_id)
if comment_data is None:
continue
# unpack object
comm_id, position, author_name, created_date, raw_text, attachments_paths, res_id = comment_data
threads_objs[comm_id] = {
"position": position,
"items": [{
"author": author_name,
"date": created_date,
"text": raw_text,
"attachments": attachments_paths,
"resource_id": res_id,
}]
}
replies = thread["replies"]["items"]
for reply in replies:
reply_data = get_info_from_comment(reply, project_id, model_id)
if reply_data is None:
continue
# unpack reply
_, position, author_name_reply, created_date_reply, raw_text_reply, attachments_paths_reply, _ = reply_data
threads_objs[comm_id]["items"].append(
{
"author": author_name_reply,
"date": created_date_reply,
"text": raw_text_reply,
"attachments": attachments_paths_reply,
}
)
return threads_objs
def get_info_from_comment(comment: Dict, project_id: str, model_id: str) -> Tuple [str, List[float], str, str, str, List[str]]:
"""Get displayable data from commit."""
comm_id = comment["id"]
author_name = comment["author"]["name"]
created_date = comment["createdAt"]
raw_text = comment["rawText"]
r'''
resources = comment["viewerResources"]
model_found = 1
# assume the model is matching, only exclude if other model_id is stated
for resource in resources:
if resource["modelId"] == model_id:
break
if resource["modelId"] is not None and resource["modelId"]!="" and resource["modelId"] != model_id:
# wrong model, don't include
model_found = 0
'''
position = [0,0,0]
res_id = model_id
viewer_state = comment["viewerState"]
if viewer_state is not None: # can be None for Replies
position: List[float] = viewer_state["ui"]["camera"]["target"]
try:
res_id = viewer_state["resources"]["request"]["resourceIdString"]
except:
pass
attachments = comment["text"]["attachments"]
attachments_paths = []
for attach in attachments:
try:
file_path = get_attachment(project_id, attach["id"], attach["fileName"])
attachments_paths.append(file_path)
except:
pass # attachment was not queried successfully
#if model_found is False:
# return None
return comm_id, position, author_name, created_date, raw_text, attachments_paths, res_id
def get_attachment(project_id: str, attachment_id: str, attachment_name: str) -> Path:
import requests
import shutil
return attachment_name
file_path_obj: Path = Path(Path(pygeoapi.__file__).parent.parent, "Temp_attachments", attachment_name)
print(file_path_obj)
file_path = str(file_path_obj)
print(file_path)
if os.path.isfile(file_path) is True: # if already saved
return file_path
url = f"https://speckle.xyz/api/stream/{project_id}/blob/{attachment_id}"
headers = {"User-Agent": "Speckle Pygeoapi"}
r = requests.get(url, headers=headers, stream=True)
if r.status_code == 200:
with open(file_path, "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
return file_path
else:
raise Exception(
f"Request not successful: Response code {r.status_code}"
)
@@ -0,0 +1,65 @@
from typing import Dict
def get_set_url_parameters(self: "SpeckleProvider"):
from pygeoapi.provider.speckle_utils.crs_utils import create_crs_from_authid
if (
isinstance(self.data, str)
and "speckleurl=" in self.data.lower()
and "projects" in self.data
and "models" in self.data
):
crs_authid = ""
for item in self.data.lower().split("&"):
# if CRS authid is found, rest will be ignored
if "datatype=" in item:
try:
self.requested_data_type = item.split("datatype=")[1]
if self.requested_data_type not in ["points", "lines", "polygons", "projectcomments"]:
self.requested_data_type = "polygons (default)"
except:
pass
elif "preserveattributes=" in item:
try:
self.preserve_attributes = item.split("preserveattributes=")[1]
if self.preserve_attributes not in ["true", "false"]:
self.preserve_attributes = "false (default)"
except:
pass
elif "limit=" in item:
try:
self.limit = int(item.split("limit=")[1])
except:
pass
elif "crsauthid=" in item:
crs_authid = item.split("crsauthid=")[1]
elif "lat=" in item:
try:
self.lat = float(item.split("lat=")[1])
except:
pass
# raise ValueError(f"Invalid Lat input, must be numeric: {item.split('lat=')[1]}")
elif "lon=" in item:
try:
self.lon = float(item.split("lon=")[1])
except:
pass
# raise ValueError(f"Invalid Lon input, must be numeric: {item.split('lon=')[1]}")
elif "northdegrees=" in item:
try:
self.north_degrees = float(item.split("northdegrees=")[1])
except:
pass
# raise ValueError(f"Invalid NorthDegrees input, must be numeric: {item.split('northdegrees=')[1]}")
# if CRS parameter present, create and assign CRS:
if len(crs_authid)>3:
create_crs_from_authid(self)
+1 -1
View File
@@ -12,7 +12,7 @@ main {
.crumbs { .crumbs {
background-color:rgb(230, 230, 230); background-color:rgb(230, 230, 230);
padding: 6px; padding: 0px;
} }
.crumbs a { .crumbs a {
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+112 -58
View File
@@ -1,5 +1,69 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<style>
.switch {
position: absolute;
display: inline-block;
right: 1vw;
top: 10px;
width: 70px;
height: 23px;
z-index: 1000000;
}
.switch span {
position: absolute;
left: 30px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: 12px;
}
.slider:before {
position: absolute;
content: "";
height: 15px;
width: 15px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(17px);
-ms-transform: translateX(17px);
transform: translateX(17px);
}
</style>
<head> <head>
<meta charset="{{ config['server']['encoding'] }}"> <meta charset="{{ config['server']['encoding'] }}">
<title>{% block title %}{% endblock %}{% if not self.title() %}{{ config['metadata']['identification']['title'] }}{% endif %}</title> <title>{% block title %}{% endblock %}{% if not self.title() %}{{ config['metadata']['identification']['title'] }}{% endif %}</title>
@@ -7,7 +71,7 @@
<meta name="language" content="{{ config['server']['language'] }}"> <meta name="language" content="{{ config['server']['language'] }}">
<meta name="description" content="{{ config['metadata']['identification']['title'] }}"> <meta name="description" content="{{ config['metadata']['identification']['title'] }}">
<meta name="keywords" content="{{ config['metadata']['identification']['keywords']|join(',') }}"> <meta name="keywords" content="{{ config['metadata']['identification']['keywords']|join(',') }}">
<link rel="shortcut icon" href="{{ config['server']['url'] }}/static/img/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="{{ config['server']['url'] }}/static/img/speckle_geo.png" type="image/x-icon">
<link rel="stylesheet" href="https://unpkg.com/bootstrap@5.1.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ config['server']['url'] }}/static/css/default.css"> <link rel="stylesheet" href="{{ config['server']['url'] }}/static/css/default.css">
<!--[if lt IE 9]> <!--[if lt IE 9]>
@@ -35,76 +99,66 @@
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<div class="bg-light sticky-top border-bottom"> <div class="bg-white sticky-top border-bottom" >
<div class="container"> <div class="container" style="max-height:fit-content;max-width: fit-content;margin-left: 10px;">
<header class="d-flex flex-wrap justify-content-center py-3"> <header class="d-flex flex-wrap justify-content-center py-2" style="text-align: left;">
<a href="{{ config['server']['url'] }}" <a href="{{ config['metadata']['contact']['url'] }}" target="_blank"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"
<img src="{{ config['server']['url'] }}/static/img/logo.png" style="text-align:left;">
title="{{ config['metadata']['identification']['title'] }}" style="height:40px;vertical-align: middle;" /></a> <img src="{{ config['server']['url'] }}/static/img/speckle_cube_32.png" alt="Speckle"
<ul class="nav nav-pills"> title="{{ config['metadata']['identification']['title'] }}" style="height:30px;vertical-align: middle;" />
<li class="nav-item"> <b style="text-align:left;padding-left: 10px;">Speckle</b>
<a href="mailto:{{ config['metadata']['contact']['email'] }}" class="nav-link" aria-current="page">{% trans %}Contact{% endtrans%}</a> </a>
</li> <p style="text-align:left;padding-left: 10px;" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
{% if config['server']['admin'] %} <a href="https://geo.speckle.systems/" target="_blank" style="text-align:left;padding-left: 10px;" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<li class="nav-item"> > Geolocating your data
<a href="{{ config['server']['url'] }}/admin/config" class="nav-link" aria-current="page">{% trans %}Admin{% endtrans %}</a> </a>
</li> </p>
{% endif %} {% if (data["model"] and data["model"]!="") %}
<!-- <a href="{{data['speckle_project_url']}}" target="_blank" style="text-align:left;padding-left: 10px;" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
Add additional menu items here > {{data["project"]}} >
<a href="https://pygeoapi.io" class="nav-link">About</a> </a>
--> <a href="{{data['speckle_url']}}" target="_blank" style="text-align:left;padding-left: 10px;color:rgb(10,132,255);" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-decoration-none">
</ul> {{data["model"]}} {{data["limit_message"]}}
</a>
{% endif %}
<div class="form-group" >
<label class="switch">3D
<input id="modeSwitch" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<ul class="nav nav-pills"> </ul>
</header> </header>
</div> </div>
</div> </div>
<div class="crumbs"> <div class="crumbs">
</div>
<main style="background-color:WhiteSmoke;">
<div style="margin-left: 20px;margin-right: 20px;">
{% block body_map %}
{% endblock %}
</div>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
{% block crumbs %}
<a href="{{ config['server']['url'] }}">{% trans %}Home{% endtrans %}</a>
{% endblock %}
<span style="float:right">
{% set links_found = namespace(json=0, jsonld=0) %}
{% for link in data['links'] %}
{% if link['rel'] == 'alternate' and link['type'] and link['type'] in ['application/json', 'application/geo+json'] %}
{% set links_found.json = 1 %}
<a href="{{ link['href'] }}">{% trans %}json{% endtrans %}</a>
{% elif link['rel'] == 'alternate' and link['type'] and link['type'] == 'application/ld+json' %}
{% set links_found.jsonld = 1 %}
<a href="{{ link['href'] }}">{% trans %}jsonld{% endtrans %}</a>
{% endif %}
{% endfor %}
{% if links_found.json == 0 %}
<a href="?f=json">{% trans %}json{% endtrans %}</a>
{% endif %}
{% if links_found.jsonld == 0 %}
<a href="?f=jsonld">{% trans %}jsonld{% endtrans %}</a>
{% endif %}
</span>
</div>
</div>
</div>
</div>
<main>
<div class="container">
<div class="row">
<div class="col-sm-12">
<br/>
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
</div> </div>
</main> </main>
<footer class="sticky-bottom bg-light d-flex flex-wrap py-3 px-3 border-top">{% trans %}Powered by {% endtrans %} <a title="pygeoapi" href="https://pygeoapi.io"><img
<footer class="sticky-bottom bg-white d-flex flex-wrap py-3 px-3 border-top">{% trans %}Powered by {% endtrans %} <a title="pygeoapi" href="https://pygeoapi.io"><img
src="{{ config['server']['url'] }}/static/img/pygeoapi.png" class="mx-1" title="pygeoapi logo" src="{{ config['server']['url'] }}/static/img/pygeoapi.png" class="mx-1" title="pygeoapi logo"
style="height:24px;vertical-align: middle;" /></a> {{ version }}</footer> style="height:24px;vertical-align: middle;" /></a> {{ version }}
</footer>
{% block extrafoot %} {% block extrafoot %}
{% endblock %} {% endblock %}
<script> <script>
@@ -162,7 +162,7 @@
]); ]);
map.addLayer(bbox_layer); map.addLayer(bbox_layer);
map.fitBounds(bbox_layer.getBounds(), {maxZoom: 10}); map.fitBounds(bbox_layer.getBounds(), {maxZoom: 22});
// Allow to get bbox query parameter of a rectangular area specified by // Allow to get bbox query parameter of a rectangular area specified by
// dragging the mouse while pressing the Ctrl key // dragging the mouse while pressing the Ctrl key
+640 -150
View File
@@ -1,179 +1,669 @@
{% extends "_base.html" %} {% extends "_base.html" %}
{% block title %}{{ super() }} {{ data['title'] }} {% endblock %}
{% block crumbs %}{{ super() }}
/ <a href="{{ data['collections_path'] }}">{% trans %}Collections{% endtrans %}</a>
{% for link in data['links'] %}
{% if link.rel == 'collection' %} /
<a href="{{ data['dataset_path'] }}">{{ link['title'] | string | truncate( 25 ) }}</a>
{% set col_title = link['title'] %}
{% endif %}
{% endfor %}
/ <a href="{{ data['items_path']}}">{% trans %}Items{% endtrans %}</a>
{% endblock %}
{% block extrahead %} {% block extrahead %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css"/> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css"/> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css"/>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster-src.js"></script> <script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster-src.js"></script>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<script src="https://cdn.maptiler.com/maptiler-sdk-js/v2.2.2/maptiler-sdk.umd.js"></script>
<link href="https://cdn.maptiler.com/maptiler-sdk-js/v2.2.2/maptiler-sdk.css" rel="stylesheet" />
{% endblock %}
{% block body_map %}
<div class="row">
{% if not data['missing_url'] %}
<div id="map2d" style="height: 80vh;"></div>
<div id="map3d" style="height: 0vh;"></div>
{% else %}
<div class="form-group" >
<label class="switch">3D
<input id="modeSwitch" type="checkbox" disabled="true">
<span class="slider round"></span>
</label>
</div>
<div id="map2d" style="height: 40vh;"></div>
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<section id="items"></section>
<section id="collection"> <section id="description">
<h1>{% for l in data['links'] if l.rel == 'collection' %} {{ l['title'] }} {% endfor %}</h1>
<p>{% trans %}Items in this collection{% endtrans %}.</p>
</section>
<section id="items">
{% if data['features'] %}
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-6"> <p> </p>
<div class="row"> </div>
<div class="col-sm-12">
<div id="items-map"></div> <div class="row">
<tr>
<p>
This is the test deployment of the OGC API server for public Speckle projects.
It allows you to share your Speckle model as geospatial data in the format of
OGC API Features / Web Feature Service, so it can be natively added to a QGIS, ArcGIS
or Civil3D project, or embedded into a web map using Leaflet, OpenLayers or other libraries.
You can find more guidelines and examples on our <a href = "https://github.com/specklesystems/pygeoapi/tree/dev" target="_blank">GitHub page</a>.
</p>
</tr>
{% if data['missing_url'] %}
<tr>
<p>
<div style="height: fit-content;">
<p> Provide Speckle Model link as an argument to start exploring, e.g.: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828</a></p>
</div> </div>
<div class="col-sm-12"> </p>
<div class="row"> </tr>
<div class="col-sm-12">
{% trans %}Warning: Higher limits not recommended!{% endtrans %} {% else %}
</div> <details>
<summary><b>Details of the current Speckle model</b></summary>
<section id="url_parameters">
{% if data['features'] %}
<div class="row">
<p> </p>
</div>
<div class="row">
<p>{% trans %}Note: if Speckle model location data is available, the relevant URL parameters will be ignored. If neither model nor URL parameters specify the location, model will be placed randomly. {% endtrans %}</p>
</div>
<div class="row">
<div class="col-md-6 col-sm-12">
<b>{% trans %}Speckle model data {% endtrans %}</b>
<div style="overflow-x: scroll;">
<table class="table table-bordered">
<thead>
</thead>
<tbody>
<tr>
<td >Project name:</td>
<td>{{ data['project'] }}</td>
</tr>
<tr>
<td >Model name:</td>
<td>{{ data['model'] }}</td>
</tr>
<tr>
<td >Last version created:</td>
<td>{{ data['model_last_version_date'] }}</td>
</tr>
<tr>
<td >Coordinate Reference System:</td>
<td>{{ data['model_crs'] }}</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="row"> </div>
<div class="col-sm-12">
{% trans %}Limit{% endtrans %}: <div class="col-md-6 col-sm-12">
<select id="limits"> <b>{% trans %}URL parameters {% endtrans %}</b>
<option value="{{ config['server']['limit'] }}">{{ config['server']['limit'] }} ({% trans %}default{% endtrans %})</option> <div style="overflow-x: scroll;">
<option value="100">100</option> <table class="table table-bordered">
<option value="1000">1,000</option> <thead>
<option value="2000">2,000</option> </thead>
</select> <tbody>
<script>
var select = document.getElementById('limits'); <tr>
var defaultValue = select.getElementsByTagName('option')[0].value; <td >Speckle URL ('speckleurl')</td>
let params = (new URL(document.location)).searchParams; <td>
select.value = params.get('limit') || defaultValue; <a href="{{ data['speckle_url'] }}">
select.addEventListener('change', ev => { {{ data['speckle_url'] }}
var limit = ev.target.value; </a>
document.location.search = `limit=${limit}`; </td>
}); </tr>
</script> <tr>
</div> <td>Requested data type ('datatype')</td>
<td>{{ data['requested_data_type'] }}</td>
</tr>
<tr>
<td>Prioritize object attributes over display quality ('preserveattributes')</td>
<td>{{ data['preserve_attributes'] }}</td>
</tr>
<tr>
<td >Coordinate Reference System ID ('crsauthid')</td>
<td>{{ data['crs_authid'] }}</td>
</tr>
<tr>
<td>Latitide ('lat')</td>
<td>{{ data['lat'] }}</td>
</tr>
<tr>
<td>Longitude ('lon')</td>
<td>{{ data['lon'] }}</td>
</tr>
<tr>
<td >Angle to True North, in degrees ('northdegrees')</td>
<td>{{ data['north_degrees'] }}</td>
</tr>
<tr>
<td >Feature limit ('limit')</td>
<td>{{ data['limit'] }}</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="row"> </div>
<div class="col-sm-12">
{% for link in data['links'] %} <div class="row">
{% if link['rel'] == 'prev' and data['offset'] > 0 %} <p> </p>
<a role="button" href="{{ link['href'] }}">{% trans %}Prev{% endtrans %}</a> </div>
{% elif link['rel'] == 'next' and data['features'] %}
<a role="button" href="{{ link['href'] }}">{% trans %}Next{% endtrans %}</a>
{% endif %} </section>
<section id="items">
<div class="row">
<p> </p>
</div>
<div class="row">
<b>{% trans %}Features in this model {% endtrans %}</b>
<div style="overflow-x: scroll;">
{% set props = [] %}
<table class="table table-striped table-bordered">
<thead>
<tr>
{% if data.get('uri_field') %}
{% set uri_field = data.uri_field %}
<th>{{ uri_field }}</th>
{% elif data.get('title_field') %}
{% set title_field = data.title_field %}
<th>{{ title_field }}</th>
{% else %}
<th>id</th>
{% endif %}
{% for k in data['features'][0]['properties'].keys() %}
{% if k not in [data.id_field, data.title_field, data.uri_field, 'extent'] %}
{% set props = props.append(k) %}
<th>{{ k | striptags }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for ft in data.features %}
<tr>
{% set title_field = data.title_field %}
<td data-label="{{ title_field }}">
<a title="{{ ft.properties.get(title_field) }}" href="{{ data['speckle_project_url'] }}/models/{{ft.id.split('_')[0]}}" target="_blank">
{{ ft.properties.get(title_field) | string | truncate( 35 ) }}
</a>
</td>
{% for prop in props %}
<td data-label="{{ prop }}">
{{ ft.properties.get(prop, '') | string | truncate( 35 ) }}
</td>
{% endfor %}
</tr>
{% endfor %} {% endfor %}
</div> </tbody>
</div> </table>
</div> </div>
</div> </div>
</div> {% else %}
<div class="row">
<p>{% trans %}No items{% endtrans %}</p>
</div>
{% endif %}
</section>
<div class="col-sm-12 col-md-6" style="overflow-x: scroll;">
{% set props = [] %}
<table class="table table-striped table-bordered">
<thead>
<tr>
{% if data.get('uri_field') %}
{% set uri_field = data.uri_field %}
<th>{{ uri_field }}</th>
{% elif data.get('title_field') %}
{% set title_field = data.title_field %}
<th>{{ title_field }}</th>
{% else %}
<th>id</th>
{% endif %}
{% for k in data['features'][0]['properties'].keys() %} </details>
{% if k not in [data.id_field, data.title_field, data.uri_field, 'extent'] %}
{% set props = props.append(k) %}
<th>{{ k | striptags }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for ft in data.features %}
<tr>
{% if data.get('uri_field') %}
{% set uri_field = data.uri_field %}
<td data-label="{{ uri_field }}">
<a href="{{ ft.properties.get(uri_field) }}" title="{{ ft.properties.get(uri_field) }}">
{{ ft.properties.get(uri_field) }}
</a>
</td>
{% elif data.get('title_field') %}
{% set title_field = data.title_field %}
<td data-label="{{ title_field }}">
<a href="{{ data.items_path }}/{{ ft['id'] }}" title="{{ ft.properties.get(title_field) }}">
{{ ft.properties.get(title_field) | string | truncate( 35 ) }}
</a>
</td>
{% else %}
<td data-label="id">
<a href="{{ data.items_path }}/{{ ft.id }}" title="{{ ft.id }}">
{{ ft.id | string | truncate( 12 ) }}
</a>
</td>
{% endif %}
{% for prop in props %} <tr><p></p></tr>
<td data-label="{{ prop }}">
{{ ft.properties.get(prop, '') | string | truncate( 35 ) }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="row col-sm-12">
<p>{% trans %}No items{% endtrans %}</p>
</div>
{% endif %} {% endif %}
</section>
<tr>
<p>
You can use the current link to access OGC API dataset in your preferred software,
as well as explore the data in the browser and share with others. URL should start with 'https://geo.speckle.systems/?'
followed by required and optional parameters. Parameters should be separated with '&' symbol.
Use the following URL parameters to construct a link that provides Speckle data with your preferred settings:
</p>
</tr>
<tr>
<i>- speckleUrl</i><p> text, required, should contain path to a specific Model in Speckle Project, e.g. 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e'
</p>
</tr>
<tr>
<i>- dataType</i><p> text, optional, choose from: points, lines, polygons or projectcomments
</p>
</tr>
<tr>
<i>- limit</i><p> positive integer, recommended, as some applications might apply their custom feature limit
</p>
</tr>
<tr>
<i>- preserveAttributes</i><p> string, optional, choose from: true, false. If not set, meshes will be split into separate polygons for better display quality.
</p>
</tr>
<tr>
<i>- crsAuthid</i><p> text, an authority string e.g. 'epsg:4326'. If set, LAT, LON and NORTHDEGREES arguments will be ignored.
</p>
</tr>
<tr>
<i>- lat</i><p> number, in range -90 to 90
</p>
</tr>
<tr>
<i>- lon</i><p> number, in range -180 to 180
</p>
</tr>
<tr>
<i>- northDegrees</i><p> number, in range -180 to 180
</p>
</tr>
<tr>
<p>
If GIS-originated Speckle model is loaded, no location arguments are needed.
</p>
</tr>
<tr>
</tr>
</tr><p></p>
<tr>
<tr>
<p>
Here are some examples:
</p>
</tr>
<tr>
<p>
1. QGIS polygon features: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons&preserveAttributes=true">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons&preserveAttributes=true</a>
</p>
</tr>
<tr>
<p>
2. QGIS point features: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/8c49788b1f&datatype=points&preserveAttributes=true">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/8c49788b1f&datatype=points&preserveAttributes=true</a>
</p>
</tr>
<tr>
<p>
3. Rhino building masses: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&lat=-0.031405&lon=109.335828</a>
</p>
</tr>
<tr>
<p>
4. Rhino detailed building: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/9c43d7569c&limit=100000&northDegrees=-117">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/9c43d7569c&limit=100000&northDegrees=-117</a>
</p>
</tr>
<tr>
<p>
5. Speckle project comments: <a href = "https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments">https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments</a>
</p>
</tr>
</tr><p></p>
<tr>
<tr>
<p>
<b>Note: this is not a production server.</b> It is still work in progress,
and we are very curious
to <a href = "https://speckle.community/invites/qxEmQb1QcM" target="_blank">hear about your use case and your feedback</a>
so we can make it better!
</p>
</tr>
</div>
</section>
{% endblock %} {% endblock %}
{% block extrafoot %} {% block extrafoot %}
{% if data['features'] %}
<script> <script>
var map = L.map('items-map').setView([{{ 45 }}, {{ -75 }}], 5); document.getElementById("loading_screen").remove();
map.addLayer(new L.TileLayer(
'{{ config['server']['map']['url'] }}', {
maxZoom: 18,
attribution: '{{ config['server']['map']['attribution'] | safe }}'
}
));
var geojson_data = {{ data['features'] | to_json | safe }};
var items = new L.GeoJSON(geojson_data, { // attach even to modeSwitch btn
onEachFeature: function (feature, layer) { document.getElementById("modeSwitch").onclick = switchMode;
var url = '{{ data['items_path'] }}/' + feature.id + '?f=html'; var el = document.getElementById("modeSwitch");
var html = '<span><a href="' + url + '">' + {% if data['title_field'] %} feature['properties']['{{ data['title_field'] }}'] {% else %} feature.id {% endif %} + '</a></span>'; if (el.addEventListener)
layer.bindPopup(html); el.addEventListener("click", switchMode, false);
} else if (el.attachEvent)
}); el.attachEvent('onclick', switchMode);
{% if data['features'][0]['geometry']['type'] == 'Point' %}
var markers = L.markerClusterGroup({ function switchMode() {
disableClusteringAtZoom: 9, btn = document.getElementById('modeSwitch');
chunkedLoading: true, if (btn.checked){
chunkInterval: 500, document.getElementById('map2d').style.height = '0vh';
}); document.getElementById('map3d').style.height = '80vh';
markers.clearLayers().addLayer(items); }
map.addLayer(markers); else {
{% else %} document.getElementById('map2d').style.height = '80vh';
map.addLayer(items); document.getElementById('map3d').style.height = '0vh';
{% endif %} }
}
var data = {{ data | to_json | safe }};
var geojson_data = {{ data['features'] | to_json | safe }};
// Leaflet 2d map
function initialize2d() {
var map = L.map('map2d').setView([45, 0], 2);
map.addLayer(new L.TileLayer(
'{{ config['server']['map']['url'] }}', {
maxZoom: 22,
attribution: '{{ config['server']['map']['attribution'] | safe }} &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>'
}
));
var items = new L.GeoJSON(geojson_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "geometry"
},
pointToLayer: (feature, latlng) => {
return new L.circleMarker(latlng)
},
onEachFeature: function (feature, layer) {
var url = '{{ data['speckle_project_url'] }}/models/' + feature.id.split("_")[0]
var html = '<span><td><p>' + feature['properties']['speckle_type'] + '</p></td><a href="' + url + '" target="_blank">' + feature['properties']['id'].split("_")[0] + '</a></span>';
layer.bindPopup(html);
var myFillColor = feature.displayProperties['color'];
var mylineWeight = feature.displayProperties['lineWidth'];
var myRadius = feature.displayProperties['radius'];
layer.setStyle({
fillColor: myFillColor,
color: myFillColor,
fillOpacity: 0.8,
weight: mylineWeight,
radius: myRadius
});
}
}); //.addTo(map);
var comments = new L.GeoJSON(geojson_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "comment"
},
pointToLayer: (feature, latlng) => {
return new L.marker(latlng)
},
onEachFeature: function (feature, layer) {
var url = '{{ data['speckle_project_url'] }}/models/' + feature.properties.resource_id + '#threadId=' + feature.id;
var html = '<span><td><a href="' + url + '" target="_blank">Go to thread</a></td> <td><p>' + feature['properties']['text_html'] + '</p></td> </span>';
layer.bindPopup(html);
}
}); //.addTo(map);
var group = L.featureGroup([items, comments])
.addTo(map);
//map.addLayer(lines);
map.fitBounds(group.getBounds());
// map.setZoom(19); // in order for the tiles to load
};
// MapTiler 3d map
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16), parseInt(result[0], 16)] : null;
};
function rgbaToArgbList(color) {
if (color == null){
return [10,132,255,255];
}
col = color.replace('rgba(','').replace(')','').split(',',4);
value = [ parseInt(col[0]), parseInt(col[1]), parseInt(col[2]), parseInt(col[3]) ];
return value;
}
function rgbaToArgbListDarker(color) {
if (color == null){
return [7,120,235,255];
}
col = color.replace('rgba(','').replace(')','').split(',',4);
value = [ parseInt(col[0])*0.95, parseInt(col[1])*0.95, parseInt(col[2])*0.95, parseInt(col[3]) ];
return value;
}
function getMessagesNumber(properties)
{
if (properties == null || properties.messages == null) {return 1}
else
{
return properties.messages.length
}
}
function getDisplayText(properties)
{
if (properties != null && properties.text_html != null){
return properties.text_html
}
else if (properties != null && properties.speckle_type != null && properties.id != null){
return `${properties.speckle_type}: ${properties.id.split("_")[0]}`
}
else{
return ``
}
}
// Callback to populate the default tooltip with content
function getTooltip({object}) {
return object && {
html: getDisplayText(object.properties),
style: {
backgroundColor: 'rgb(255,255,255)',
color: 'rgb(0,0,0)',
fontSize: '0.9em'
}
};
}
function createSVGIcon(n) {
const label = n < 10 ? n.toString() : '10+';
return `\
<svg width="100" height="100" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="rgb(10,132,255)" stroke="rgb(200,200,200)" stroke-width="2"/>
<text x="12" y="12" fill="#fff" text-anchor="middle" alignment-baseline="middle" font-family="verdana" font-size="8">${label}</text>
</svg>`;
}
// Note that a xml string cannot be directly embedded in a data URL
// it has to be either escaped or converted to base64.
function svgToDataUrl(svg) {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
// You may need base64 encoding if the SVG contains certain special characters
function svgToDataUrlBase64(svg) {
return `data:image/svg+xml;base64,${btoa(svg)}`;
}
function initialize3d() {
maptilersdk.config.apiKey = '{{ config["server"]["map"]["key"] }}';
speckle_data = JSON.parse(JSON.stringify(data))
var speckle_features = []
for (let i = 0; i < speckle_data.features.length; i++) {
feature = speckle_data.features[i];
coords = feature.geometry.coordinates;
if (feature.geometry.type.includes("Polygon")) {
polygons = []
// check orientation of each PolygonPart, if vertical - shift points slightly
for (let p = 0; p < coords.length; p++) {
for (let c = 0; c < coords[p].length; c++) {
sum_orientation = 0;
polygon_pts = coords[p][c]; // usually 3 for Mesh faces
for (let k = 0; k < polygon_pts.length; k++){
index = k + 1
if (k == polygon_pts.length - 1){index = 0}
pt = polygon_pts[k]
pt2 = polygon_pts[index]
sum_orientation += (pt2[0] - pt[0]) * (pt2[1] + pt[1])
};
createdPolygon = false;
if (-0.01 < sum_orientation && sum_orientation <0.01){
coords[p][c][0][0] += 0.0000001;
coords[p][c][0][1] += 0.0000001;
coords[p][c][1][0] -= 0.0000001;
coords[p][c][1][1] -= 0.0000001;
coords[p][c][2][0] += 0.0000001;
coords[p][c][2][1] += 0.0000001;
if(polygon_pts.length==3) {
createdPolygon = true;
multipolygon_coords = [coords[p][c]];
polygons.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": "MultiPolygon", "coordinates":[multipolygon_coords]},
"properties": speckle_data.features[i].properties,
"displayProperties": speckle_data.features[i].displayProperties });
}
else if (polygon_pts.length==4) {
createdPolygon = true;
multipolygon_coords = [coords[p][c].slice(0,3)];
polygons.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": "MultiPolygon", "coordinates":[multipolygon_coords]},
"properties": speckle_data.features[i].properties,
"displayProperties": speckle_data.features[i].displayProperties });
multipolygon_coords = [[coords[p][c][2], coords[p][c][3], coords[p][c][0]]];
polygons.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": "MultiPolygon", "coordinates":[multipolygon_coords]},
"properties": speckle_data.features[i].properties ,
"displayProperties": speckle_data.features[i].displayProperties });
};
};
if (createdPolygon == false){
multipolygon_coords = [coords[p][c]];
polygons.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": "MultiPolygon", "coordinates":[multipolygon_coords]},
"properties": speckle_data.features[i].properties,
"displayProperties": speckle_data.features[i].displayProperties });
}
};
};
polygons.forEach((element, index, array) => {
element.displayProperties.lineWidth = 0.05
speckle_features.push(element);
});
}
else if (speckle_data.features[i].displayProperties.object_type == "comment")
{
speckle_features.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": speckle_data.features[i].geometry.type, "coordinates":speckle_data.features[i].geometry.coordinates},
"properties": speckle_data.features[i].properties,
"displayProperties":
{
"color": 'rgba(10,132,255,255)',
"lineWidth": 2,
"radius": 10,
"object_type": "comments",
}
});
}
else
{
speckle_features.push({"id": speckle_features.length, "type":"Feature",
"geometry": {"type": speckle_data.features[i].geometry.type, "coordinates":speckle_data.features[i].geometry.coordinates},
"properties": speckle_data.features[i].properties,
"displayProperties": speckle_data.features[i].displayProperties });
}
}
var extent = speckle_data["extent"];
speckle_data.features = [];
speckle_features.forEach((element, index, array) => {
if (element.displayProperties.object_type != "comments"){
speckle_data.features.push(element);
}
});
speckle_comments = [];
speckle_features.forEach((element, index, array) => {
if (element.displayProperties.object_type == "comments"){
speckle_comments.push(element);
}
});
// Create deck.gl map
const deckgl = new deck.DeckGL({
container: 'map3d',
map: maptilersdk,
mapStyle: maptilersdk.MapStyle.STREETS.PASTEL,
initialViewState: {
longitude: extent[0] + (extent[2]-extent[0])/2,
latitude: extent[1] + (extent[3]-extent[1])/2,
zoom: 22,
pitch: 60,
bearing: 1.469387755102039
},
controller: true,
layers: [
new deck.GeoJsonLayer({
id: 'speckle_data',
data: speckle_data,
// Styles
filled: true,
getFillColor: f => rgbaToArgbList(f.displayProperties.color),
getLineWidth: f => f.displayProperties.lineWidth,
getLineColor: f => rgbaToArgbListDarker(f.displayProperties.color),
getPointRadius: f => f.displayProperties.radius / 2,
// Interactive props
pickable: true,
autoHighlight: true,
}),
new deck.IconLayer({
id: 'IconLayer',
data: speckle_comments,
// getColor: d => [Math.sqrt(getMessagesNumber(d.properties)), 140, 0],
getIcon: d => ({
url: svgToDataUrl(createSVGIcon( getMessagesNumber(d.properties).toString() )), //'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png',
width: 128,
height: 128
}),
getPosition: d => d.geometry.coordinates[0],
getSize: f => 50 * Math.pow(getMessagesNumber(f.properties),0.3),
pickable: true
})
],
getTooltip
});
}
initialize2d();
initialize3d();
map.fitBounds(items.getBounds());
</script> </script>
{% endif %}
{% endblock %} {% endblock %}
@@ -125,7 +125,7 @@
var map = L.map('items-map').setView([{{ 45 }}, {{ -75 }}], 10); var map = L.map('items-map').setView([{{ 45 }}, {{ -75 }}], 10);
map.addLayer(new L.TileLayer( map.addLayer(new L.TileLayer(
'{{ config['server']['map']['url'] }}', { '{{ config['server']['map']['url'] }}', {
maxZoom: 18, maxZoom: 22,
attribution: '{{ config['server']['map']['attribution'] | safe }}' attribution: '{{ config['server']['map']['attribution'] | safe }}'
} }
)); ));
@@ -133,6 +133,6 @@
var items = new L.GeoJSON(geojson_data); var items = new L.GeoJSON(geojson_data);
map.addLayer(items); map.addLayer(items);
map.fitBounds(items.getBounds(), {maxZoom: 15}); map.fitBounds(items.getBounds(), {maxZoom: 18});
</script> </script>
{% endblock %} {% endblock %}
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<body>
<div id="loading_screen" style="position: absolute;left: 20px; right: 20px;top:20px;width: fit-content;margin-inline: auto;">
{{data['exception']}}
</div>
</body>
<script>
document.getElementById("loading_screen").remove();
</script>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<body>
<div id="loading_screen" style="position: absolute;top:100px;left:0;right:0;width: fit-content;margin-inline: auto;height:200px;">
<img style="height:200px;width:218.6px" src="https://raw.githubusercontent.com/specklesystems/pygeoapi/dev/pygeoapi/static/img/speckle_cube_loading.gif" alt="Loading data.." >
<h3 style="margin-bottom: 0px;font-family: 'Verdana'; text-align: center; color:rgb(40, 127, 209);font-size: x-large;">"I'm on my way!"</h3>
<p style="font-family: 'Verdana'; text-align: right; color:rgb(40, 127, 209)">- your data </p>
</div>
</body>
</html>
+1
View File
@@ -17,3 +17,4 @@ shapely
SQLAlchemy<2.0.0 SQLAlchemy<2.0.0
tinydb tinydb
unicodecsv unicodecsv
specklepy==2.19.6
+1
View File
@@ -0,0 +1 @@
Examples of adding Speckle layers to maps built with Leaflet and OpenLayers.
+175
View File
@@ -0,0 +1,175 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Leaflet demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
</head>
<body>
<main style="margin-left: 20px;margin-right: 20px;">
<div class="row">
<h3>Speckle pygeoapi demo: construct URL</h3>
<div class="form-group" ><label style="min-width: 10vw;display:inline-block">Speckle Model URL: </label>
<input value="https://app.speckle.systems/projects/64753f52b7/models/338b386787" id="speckle_model" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Data type: </label>
<select id="data_type" style="min-width: 55vw;">
<option value="polygons">polygons</option>
<option value="points">points</option>
<option value="lines">lines</option>
<option value="projectcomments">projectcomments</option>
</select>
</div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Preserve attributes: </label>
<select id="preserve_attributes" style="min-width: 55vw;">
<option value="false">false</option>
<option value="true">true</option>
</select>
</div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Feature limit: </label>
<input value="100000" id="limit" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Lat: </label>
<input value="-0.031405" id="lat" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Lon: </label>
<input value="109.335828" id="lon" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">North (degrees): </label>
<input value="" id="north_degrees" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<div class="form-group"><label style="min-width: 10vw;display:inline-block">Final URL: </label>
<input value="" id="link" type="text" maxlength="512" style="min-width: 55vw;" class="searchField"/></div>
<input id="clickMe" type="button" value="Refresh map" onclick="doFunction();" />
</div>
<div>
<div class="row"><p></p></div>
<div class="row">
<div id="items-map" style="min-height: 60vh;"></div>
</div>
</div>
</main>
<script>
// attach event to a button click
document.getElementById("clickMe").onclick = doFunction;
var el = document.getElementById("clickMe");
if (el.addEventListener)
el.addEventListener("click", doFunction, false);
else if (el.attachEvent)
el.attachEvent('onclick', doFunction);
// create map
var map = L.map('items-map').setView([ 45 , -75 ], 5);
map.addLayer(new L.TileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>'
}
));
// load Speckle data
document.getElementById('link').disabled=true;
doFunction();
async function doFunction() {
map.eachLayer(function (layer) {
if (!(layer instanceof L.TileLayer)){
map.removeLayer(layer);
}
});
// construnt data URL
var link = "https://geo.speckle.systems/?speckleUrl=";
if (document.getElementById("speckle_model").value!=""){
link += document.getElementById("speckle_model").value.replace(" ", "")
}
if (document.getElementById("data_type").value!=""){
link += "&datatype=" + document.getElementById("data_type").value.replace(" ", "")
}
if (document.getElementById("limit").value!=""){
link += "&limit=" + document.getElementById("limit").value.replace(" ", "")
}
if (document.getElementById("preserve_attributes").value!=""){
link += "&preserveAttributes=" + document.getElementById("preserve_attributes").value.replace(" ", "")
}
if (document.getElementById("lon").value!=""){
link += "&lon=" + document.getElementById("lon").value.replace(" ", "")
}
if (document.getElementById("lat").value!=""){
link += "&lat=" + document.getElementById("lat").value.replace(" ", "")
}
if (document.getElementById("north_degrees").value!=""){
link += "&northDegrees=" + document.getElementById("north_degrees").value.replace(" ", "")
}
// set URL value to the text field
document.getElementById("link").value = link;
// get Speckle data
const speckle_data = await fetch(link, {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
// add data to map
speckle_layer = L.geoJSON(speckle_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "comment"
},
pointToLayer: (feature, latlng) => {
return new L.marker(latlng)
},
onEachFeature: function (feature, layer) {
var html = '<span><td><p>' + feature['properties']['text_html'] + '</p></td> </span>';
layer.bindPopup(html);
}
});
speckle_layer.addTo(map);
speckle_layer2 = L.geoJSON(speckle_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "geometry"
},
pointToLayer: (feature, latlng) => {
return new L.circleMarker(latlng)
},
onEachFeature: function (feature, layer) {
var html = '<span><td><p>' + feature['properties']['speckle_type'] + '</p></td></span>';
layer.bindPopup(html);
var myFillColor = feature.displayProperties['color'];
var mylineWeight = feature.displayProperties['lineWidth'];
var myRadius = feature.displayProperties['radius'];
layer.setStyle({
fillColor: myFillColor,
color: myFillColor,
fillOpacity: 0.8,
weight: mylineWeight,
radius: myRadius
});
}
});
speckle_layer2.addTo(map);
if (document.getElementById("data_type").value.replace(" ","").toLowerCase() == "projectcomments"){
map.fitBounds(speckle_layer.getBounds())
}else{
map.fitBounds(speckle_layer2.getBounds())
}
};
</script>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Leaflet demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
</head>
<body>
<main style="margin-left: 20px;margin-right: 20px;">
<div class="row">
<h3>Speckle pygeoapi demo: fetch comments</h3>
</div>
<div>
<div class="row">
<div id="items-map" style="min-height: 80vh;"></div>
</div>
</div>
</main>
<script>
var map = L.map('items-map').setView([ 45 , -75 ], 5);
map.addLayer(new L.TileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>'
}
));
(async () => {
const speckle_data = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments', {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
speckle_layer = L.geoJSON(speckle_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "comment"
},
pointToLayer: (feature, latlng) => {
return new L.marker(latlng)
},
onEachFeature: function (feature, layer) {
var html = '<span><td><p>' + feature['properties']['text_html'] + '</p></td> </span>';
layer.bindPopup(html);
}
});
speckle_layer.addTo(map);
map.fitBounds(speckle_layer.getBounds())
})();
</script>
</body>
</html>
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Leaflet demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
</head>
<body>
<main style="margin-left: 20px;margin-right: 20px;">
<div class="row">
<h3>Speckle pygeoapi demo: display masterplan</h3>
</div>
<div>
<div class="row">
<div id="items-map" style="min-height: 80vh;"></div>
</div>
</div>
</main>
<script>
var map = L.map('items-map').setView([ 45 , -75 ], 5);
map.addLayer(new L.TileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>'
}
));
(async () => {
const speckle_data = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons', {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
speckle_layer = L.geoJSON(speckle_data, {
filter: (feature) => {
return feature.displayProperties["object_type"] == "geometry"
},
pointToLayer: (feature, latlng) => {
return new L.circleMarker(latlng)
},
onEachFeature: function (feature, layer) {
var html = '<span><td><p>' + feature['properties']['speckle_type'] + '</p></td></span>';
layer.bindPopup(html);
var myFillColor = feature.displayProperties['color'];
var mylineWeight = feature.displayProperties['lineWidth'];
var myRadius = feature.displayProperties['radius'];
layer.setStyle({
fillColor: myFillColor,
color: myFillColor,
fillOpacity: 0.8,
weight: mylineWeight,
radius: myRadius
});
}
});
speckle_layer.addTo(map);
map.fitBounds(speckle_layer.getBounds())
})();
</script>
</body>
</html>
+287
View File
@@ -0,0 +1,287 @@
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maptalks demo</title>
<style type="text/css">
html,body{margin:0px;height:100%;width:100%}
.container{width:100%;height:100%}
</style>
<link rel="stylesheet" href="https://unpkg.com/maptalks/dist/maptalks.css">
<script type="text/javascript" src="https://unpkg.com/maptalks/dist/maptalks.min.js"></script>
<body>
<div class="row">
<h3>Speckle pygeoapi demo: display comments</h3>
</div>
<div id="map" class="container"></div>
<script>
var map = new maptalks.Map('map', {
center: [-0.113049, 51.498568],
zoom: 14,
pitch : 56,
bearing : 60,
baseLayer: new maptalks.TileLayer('base', {
urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
subdomains: ["a","b","c","d"],
attribution: '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>'
})
});
(async () => {
const speckle_data2 = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/37d93c5d32&preserveAttributes=true', {
//const speckle_data = await fetch('http://localhost:5000/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&limit=1000000&lat=-0.031405&lon=109.335828&preserveAttributes=true', {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
console.log(speckle_data2)
var speckle_features = []
for (let i = 0; i < speckle_data2.features.length; i++) {
feature = speckle_data2.features[i];
coords = feature.geometry.coordinates;
if (feature.geometry.type.includes("Polygon")) {
// check orientation of each PolygonPart
for (let c = 0; c < coords[0].length; c++) {
sum_orientation = 0;
polygon_pts = coords[0][c]; // usually 3 for Mesh faces
for (let k = 0; k < polygon_pts.length; k++){
index = k + 1
if (k == polygon_pts.length - 1){index = 0}
pt = polygon_pts[k]
pt2 = polygon_pts[index]
sum_orientation += (pt2[0] - pt[0]) * (pt2[1] + pt[1])
};
if (sum_orientation <0.01 ){
coords[0][c][0][0] += 0.000001;
coords[0][c][0][1] += 0.000001;
};
};
speckle_features.push(
new maptalks.MultiPolygon(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.9,
'lineWidth' : 1.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
} else if (feature.geometry.type.includes("MultiLineString")){
speckle_features.push(
new maptalks.MultiLineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("LineString")){
speckle_features.push(
new maptalks.LineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("MultiPoint")){
symbol= {
'markerType': 'ellipse',
'markerFill': speckle_data.features[i].displayProperties.color,
'markerFillOpacity': 0.7,
'markerLineColor': speckle_data.features[i].displayProperties.color,
'markerLineWidth': 0,
'markerLineOpacity': 1,
'markerLineDasharray':[],
'markerWidth': 10,
'markerHeight': 10,
'markerDx': -10,
'markerDy': 0,
'markerOpacity' : 1,
'textFaceName' : 'sans-serif',
'textName' : speckle_data.features[i].properties["FID"],
'textFill' : '#34495e',
'textHorizontalAlignment' : 'right',
'textSize' : 20
};
if (feature.displayProperties["object_type"] == "comment"){
var strippedHtml = feature.properties.text_html.replaceAll('&emsp;', ' ').replaceAll('<br>', '\n').replace(/<[^>]+>/g, '');
symbol.textName = strippedHtml;
symbol.textSize = 10;
};
speckle_features.push(
new maptalks.MultiPoint(coords, {
visible : true,
cursor : null,
cursor : 'pointer',
symbol: symbol
})
);
}
}
speckle_layer2 = new maptalks.VectorLayer('vector2', speckle_features, { enableAltitude : true
, drawAltitude : {
//polygonFill : '#1bbc9b',
//polygonOpacity : 0.3,
//lineWidth : 0
}
}
);
speckle_layer2.addTo(map);
map.fitExtent(speckle_layer2.getExtent(), 0);
//map.fitBounds(speckle_layer.getBounds())
const speckle_data = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments', {
//const speckle_data = await fetch('http://localhost:5000/?speckleUrl=https://app.speckle.systems/projects/64753f52b7/models/338b386787&limit=1000000&lat=-0.031405&lon=109.335828&preserveAttributes=true', {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
console.log(speckle_data)
var speckle_features = []
for (let i = 0; i < speckle_data.features.length; i++) {
feature = speckle_data.features[i];
coords = feature.geometry.coordinates;
if (feature.geometry.type.includes("Polygon")) {
// check orientation of each PolygonPart
for (let c = 0; c < coords[0].length; c++) {
sum_orientation = 0;
polygon_pts = coords[0][c]; // usually 3 for Mesh faces
for (let k = 0; k < polygon_pts.length; k++){
index = k + 1
if (k == polygon_pts.length - 1){index = 0}
pt = polygon_pts[k]
pt2 = polygon_pts[index]
sum_orientation += (pt2[0] - pt[0]) * (pt2[1] + pt[1])
};
if (sum_orientation <0.01 ){
coords[0][c][0][0] += 0.000001;
coords[0][c][0][1] += 0.000001;
};
};
speckle_features.push(
new maptalks.MultiPolygon(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.3,
'lineWidth' : 0.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
} else if (feature.geometry.type.includes("MultiLineString")){
speckle_features.push(
new maptalks.MultiLineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("LineString")){
speckle_features.push(
new maptalks.LineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("MultiPoint")){
symbol= {
'markerType': 'ellipse',
'markerFill': speckle_data.features[i].displayProperties.color,
'markerFillOpacity': 0.7,
'markerLineColor': speckle_data.features[i].displayProperties.color,
'markerLineWidth': 0,
'markerLineOpacity': 1,
'markerLineDasharray':[],
'markerWidth': 10,
'markerHeight': 10,
'markerDx': -10,
'markerDy': 0,
'markerOpacity' : 1,
'textFaceName' : 'sans-serif',
'textName' : speckle_data.features[i].properties["FID"],
'textFill' : '#34495e',
'textHorizontalAlignment' : 'right',
'textSize' : 20
};
if (feature.displayProperties["object_type"] == "comment"){
var strippedHtml = feature.properties.text_html.replaceAll('&emsp;', ' ').replaceAll('<br>', '\n').replace(/<[^>]+>/g, '');
symbol.textName = strippedHtml;
symbol.textSize = 10;
};
speckle_features.push(
new maptalks.MultiPoint(coords, {
visible : true,
cursor : null,
cursor : 'pointer',
symbol: symbol
})
);
}
}
speckle_layer = new maptalks.VectorLayer('vector', speckle_features, { enableAltitude : true
, drawAltitude : {
//polygonFill : '#1bbc9b',
//polygonOpacity : 0.3,
//lineWidth : 0
}
}
);
speckle_layer.addTo(map);
map.fitExtent(speckle_layer.getExtent(), 0);
//map.fitBounds(speckle_layer.getBounds())
})();
</script>
</body>
</html>
+223
View File
@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maptalks demo</title>
<style type="text/css">
html,body{margin:0px;height:100%;width:100%}
.container{width:100%;height:100%}
</style>
<link rel="stylesheet" href="https://unpkg.com/maptalks/dist/maptalks.css">
<script type="text/javascript" src="https://unpkg.com/maptalks/dist/maptalks.min.js"></script>
<body>
<div class="row">
<h3>Speckle pygeoapi demo: display polygons</h3>
</div>
<div id="map" class="container"></div>
<script>
var map = new maptalks.Map('map', {
center: [-0.113049, 51.498568],
zoom: 14,
pitch : 56,
bearing : 60,
baseLayer: new maptalks.TileLayer('base', {
urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
subdomains: ["a","b","c","d"],
attribution: '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>'
})
});
(async () => {
//const speckle_data = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments', {
//var speckle_url = 'http://localhost:5000/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/9c43d7569c&limit=1000000&datatype=polygons&preserveattributes=false';
// https://app.speckle.systems/projects/5feae56049/models/01c4183677
var speckle_url = 'http://localhost:5000/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/01c4183677&limit=1000000&datatype=polygons&preserveattributes=true';
//var speckle_url = 'https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/9c43d7569c&northDegrees=-30&preserveAttributes=true';
const speckle_data = await fetch(speckle_url, {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
var speckle_features = []
for (let i = 0; i < speckle_data.features.length; i++) {
feature = speckle_data.features[i];
coords = feature.geometry.coordinates;
if (feature.geometry.type.includes("Polygon")) {
// check orientation of each PolygonPart, if vertical - shift points slightly
for (let p = 0; p < coords.length; p++) {
for (let c = 0; c < coords[p].length; c++) {
// each polygon Part unpacked
sum_orientation = 0;
polygon_pts = coords[p][c]; // usually 3 for Mesh faces
for (let k = 0; k < polygon_pts.length; k++){
index = k + 1
if (k == polygon_pts.length - 1){index = 0}
pt = polygon_pts[k]
pt2 = polygon_pts[index]
sum_orientation += (pt2[0] - pt[0]) * (pt2[1] + pt[1])
};
createdPolygon = false;
if (-0.01 < sum_orientation && sum_orientation <0.01){
coords[p][c][0][0] += 0.0000001;
coords[p][c][0][1] += 0.0000001;
coords[p][c][1][0] -= 0.0000001;
coords[p][c][1][1] -= 0.0000001;
if(polygon_pts.length==3) {
createdPolygon = true;
speckle_features.push(
new maptalks.MultiPolygon([coords[p][c]], {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.3,
'lineWidth' : 0.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
}
else if (polygon_pts.length==4) {
createdPolygon = true;
speckle_features.push(
new maptalks.MultiPolygon([coords[p][c].slice(0,3)], {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.3,
'lineWidth' : 0.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
speckle_features.push(
new maptalks.MultiPolygon([[coords[p][c][2], coords[p][c][3], coords[p][c][0]]], {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.3,
'lineWidth' : 0.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
}
};
if (createdPolygon == false){
speckle_features.push(
new maptalks.MultiPolygon([coords[0][c]], {
visible : true,
cursor : null,
symbol: {
'lineColor' : 'rgb(200,200,200)',
'lineOpacity' : 0.3,
'lineWidth' : 0.5,
'polygonFill' : feature.displayProperties.color,
'polygonOpacity' : 1
}
})
);
}
};
};
} else if (feature.geometry.type.includes("MultiLineString")){
speckle_features.push(
new maptalks.MultiLineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("LineString")){
speckle_features.push(
new maptalks.LineString(coords, {
visible : true,
cursor : null,
symbol: {
'lineColor' : feature.displayProperties.color,
'lineOpacity' : 1,
'lineWidth' : 2
}
})
);
} else if (feature.geometry.type.includes("MultiPoint")){
symbol= {
'markerType': 'ellipse',
'markerFill': speckle_data.features[i].displayProperties.color,
'markerFillOpacity': 0.7,
'markerLineColor': speckle_data.features[i].displayProperties.color,
'markerLineWidth': 0,
'markerLineOpacity': 1,
'markerLineDasharray':[],
'markerWidth': 10,
'markerHeight': 10,
'markerDx': -10,
'markerDy': 0,
'markerOpacity' : 1,
'textFaceName' : 'sans-serif',
'textName' : speckle_data.features[i].properties["FID"],
'textFill' : '#34495e',
'textHorizontalAlignment' : 'right',
'textSize' : 20
};
if (feature.displayProperties["object_type"] == "comment"){
var strippedHtml = feature.properties.text_html.replaceAll('&emsp;', ' ').replaceAll('<br>', '\n').replace(/<[^>]+>/g, '');
symbol.textName = strippedHtml;
symbol.textSize = 10;
};
speckle_features.push(
new maptalks.MultiPoint(coords, {
visible : true,
cursor : null,
cursor : 'pointer',
symbol: symbol
})
);
}
}
speckle_layer = new maptalks.VectorLayer('vector', speckle_features, { enableAltitude : true
, drawAltitude : {
//polygonFill : '#1bbc9b',
//polygonOpacity : 0.3,
//lineWidth : 0
}
}
);
speckle_layer.addTo(map);
map.fitExtent(speckle_layer.getExtent(), 0);
//map.fitBounds(speckle_layer.getBounds())
})();
</script>
</body>
</html>
@@ -0,0 +1,148 @@
<!-- Modified sample from: https://openstreetmap.be/en/projects/howto/openlayers.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css"
type="text/css">
<title>OpenLayers demo</title>
<style>
.ol-attribution.ol-logo-only,
.ol-attribution.ol-uncollapsible {
max-width: calc(100% - 3em) !important;
height: 1.5em !important;
}
.ol-control button,
.ol-attribution,
.ol-scale-line-inner {
font-family: 'Lucida Grande', Verdana, Geneva, Lucida, Arial, Helvetica, sans-serif !important;
}
</style>
</head>
<body>
<div class="row">
<h3>Speckle pygeoapi demo with OpenLayers</h3>
</div>
<div id="map" style="margin-left: 20px;margin-right: 20px;"></div>
<div id="popup" class="ol-popup">
<a href="#" id="popup-closer" class="ol-popup-closer"></a>
<div id="popup-content"></div>
</div>
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script>
var attribution = new ol.control.Attribution({
collapsible: false
});
var map = new ol.Map({
controls: ol.control.defaults({ attribution: false }).extend([attribution]),
layers: [
new ol.layer.Tile({
source: new ol.source.OSM({
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attributions: [ol.source.OSM.ATTRIBUTION],
maxZoom: 18
})
})
],
target: 'map',
view: new ol.View({
center: ol.proj.fromLonLat([0,0]),
maxZoom: 22,
zoom: 3
})
});
//////// add Speckle layer
(async () => {
const geojson = await fetch('https://geo.speckle.systems/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=polygons', {
headers: {
'Accept': 'application/geo+json'
}
}).then(response => response.json());
speckle_data = {
"type": "FeatureCollection",
"features": geojson["features"],
"model": geojson["model"],
"model_id": geojson["model_id"],
"project": geojson["project"],
"requested_data_type": geojson["requested_data_type"],
"speckle_url": geojson["speckle_url"],
"speckle_project_url": geojson["speckle_project_url"],
"timestamp": geojson["timestamp"],
"lat": geojson["lat"],
"lon": geojson["lon"],
"limit": geojson["limit"],
"north_degrees": geojson["north_degrees"],
"model_crs": geojson["model_crs"],
"extent": geojson["extent"],
}
var width = 1
var myStyle = [
new ol.style.Style({
fill: new ol.style.Fill({
color: 'rgba(10,132,255,0.3)'
}),
stroke: new ol.style.Stroke({
color: '#0a84ff',
width: 3
}),
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({color: '#0a84ff'}),
stroke: new ol.style.Stroke({
color: [10,132,255], width: 2
})
})
}),
];
speckleLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: new ol.format.GeoJSON().readFeatures(speckle_data, { featureProjection: 'EPSG:3857' }),
attributions: ' &copy; Data: <a href="https://speckle.systems/">Speckle Systems</a>.'
}),
style: myStyle
});
map.addLayer(speckleLayer);
function epsg4326toEpsg3857(coordinates) {
let x = coordinates[0];
let y = coordinates[1];
x = (coordinates[0] * 20037508.34) / 180;
y =
Math.log(Math.tan(((90 + coordinates[1]) * Math.PI) / 360)) /
(Math.PI / 180);
y = (y * 20037508.34) / 180;
return [x, y];
}
min_xy = [speckle_data["extent"][0], speckle_data["extent"][1]]
max_xy = [speckle_data["extent"][2], speckle_data["extent"][3]]
min_xy_new = epsg4326toEpsg3857(min_xy);
max_xy_new = epsg4326toEpsg3857(max_xy);
if (speckleLayer.getSource().getFeatures().length > 0) {
map.getView().fit( [min_xy_new[0], min_xy_new[1], max_xy_new[0], max_xy_new[1]], map.getSize() );
}
})();
</script>
</body>
</html>
Executable
+8
View File
@@ -0,0 +1,8 @@
source .venv/bin/activate
export PYGEOAPI_CONFIG="example-config.yml"
export PYGEOAPI_OPENAPI="example-openapi.yml"
export MAPTILER_KEY_SPECKLE="qam9vwl7bVk5tW1oZu46"
export PORT=8000
gunicorn pygeoapi.flask_app:APP --timeout 100000 --access-logfile access_log --error-logfile error_log --capture-output