Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cc56e6e97 | |||
| 3da41021b1 | |||
| 36fb66e72e | |||
| 8325724ab0 | |||
| 1c749f09c8 | |||
| 62a37aa7fb | |||
| 660b37384b | |||
| d525d58111 | |||
| 46aecdac41 | |||
| a4122673ec | |||
| b07ed87d03 | |||
| 6833aaef21 | |||
| eda0268524 | |||
| a29b0c3e73 | |||
| 9f63ef8ced | |||
| e94ca610b7 | |||
| 46a7b968e1 | |||
| 82fcfe6661 | |||
| 3e4af97056 | |||
| 99b397b648 | |||
| 1bcd4a67f5 | |||
| a7de3ee43e | |||
| 39b81f0a15 | |||
| 4bfcd9e6b7 | |||
| cc4a7f6d3a | |||
| d0a0d7efae | |||
| 805b148366 | |||
| 7740958608 | |||
| 63d2879709 | |||
| 396916bb7c | |||
| c7c7ef8b85 | |||
| 3389528861 | |||
| 8c691454e6 | |||
| 5ef776ada0 | |||
| e5077e1677 | |||
| 672c29671e | |||
| 0d2de03cd6 | |||
| 860bcc1086 | |||
| e11a1338a5 | |||
| 525955c058 | |||
| 3bcea48c32 | |||
| 6e48c4b8b0 | |||
| f7f499aa81 | |||
| fb933e0472 | |||
| 7f4dfe4272 | |||
| 92e016b004 | |||
| eb287480c9 | |||
| c84e81451d | |||
| 6ec6725947 | |||
| f814592ae1 | |||
| 4097aff759 | |||
| 8f845c95bb | |||
| e2bf4ee3b1 | |||
| 5b37680d73 | |||
| 661d0e9104 | |||
| 434f712cbf | |||
| ebfb49c4a1 | |||
| 8759fc9e09 | |||
| 4e43085ef2 | |||
| 079e8e5dda | |||
| f5e52de572 | |||
| 66d7bd69fb | |||
| 6b7112ccef | |||
| 6bf99294db | |||
| 8bbd50265e | |||
| dd2f3f3bb0 | |||
| 5ec81ea745 | |||
| a20daffc4b | |||
| 6a8b5df87c | |||
| 79b0116d9a | |||
| 9a89b4a7ac | |||
| cdc8717bde | |||
| 8e643e1e38 | |||
| 7141e081cc | |||
| 782a392825 | |||
| 5ee0e75025 | |||
| 3afc8ca5f1 | |||
| 7bdbbb7cdf | |||
| 1679045504 | |||
| 3bde1c3b93 | |||
| c2a5642faa | |||
| 0eb30470c6 | |||
| 8c3202290d | |||
| 652c52937c | |||
| 607dc2e9b4 | |||
| c3e415237d | |||
| acca99a891 | |||
| 0a96fda59d | |||
| dbe99613b9 | |||
| 8ee256648d | |||
| a174694961 | |||
| d7a5cb6576 | |||
| e8f7d528bd | |||
| 4347cf3bb6 | |||
| 61da86554d | |||
| b4f3b45bfd | |||
| 8507fdfb45 | |||
| adbfdf95f5 | |||
| 2019d106ad | |||
| 6f1042eb13 | |||
| 695adb0676 | |||
| 99fbcd0264 | |||
| 0cb2802427 | |||
| b8f9144599 | |||
| 46e419e2f3 | |||
| 36a3567c2c | |||
| 95f603b167 | |||
| 31e35668ba | |||
| 3f42069d9e | |||
| ec04220183 | |||
| fad6300654 | |||
| 7bc42653d0 | |||
| c96d4f6f30 | |||
| 92e0b0da9e |
@@ -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: https://geo.speckle.systems
|
||||||
|
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
@@ -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
|
||||||
|
|||||||
@@ -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: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> © Data: <a href="https://speckle.systems/">Speckle Systems</a>'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
loadSpeckleData();
|
||||||
|
|
||||||
|
async function loadSpeckleData() => {
|
||||||
|
var speckle_model_url = 'https://geo.speckle.systems/speckle/?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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Connect, select the dataset "Speckle data" and click "Add".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Loading of the data might take a minute, then you will be able to Zoom to layer and check the Attribute table. Done!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Add Speckle OGC API layer in ArcGIS
|
||||||
|
|
||||||
|
1. Add new OGC API Connection
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. Add URL, preferably add the URL parameter with the custom feature limit (e.g. '&limit=10000')
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Find Speckle Pygeoapi server in Catalog, add SpeckleData layer to Map
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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: '© <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
|
||||||
@@ -75,7 +75,7 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
#: Return headers for requests (e.g:X-Powered-By)
|
#: Return headers for requests (e.g:X-Powered-By)
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Powered-By': f'pygeoapi {__version__}'
|
# 'X-Powered-By': f'pygeoapi {__version__}'
|
||||||
}
|
}
|
||||||
|
|
||||||
CHARSET = ['utf-8']
|
CHARSET = ['utf-8']
|
||||||
|
|||||||
@@ -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.lower() == 'speckleurl' and len(request.params[item])>40 and ('speckleurl=' + request.params[item]) in provider_def['data'].lower():
|
||||||
|
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'
|
||||||
@@ -554,7 +566,14 @@ 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("&")
|
||||||
|
|
||||||
# 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
@@ -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")
|
||||||
|
|
||||||
|
map_api_key_local = os.environ.get("MAPTILER_KEY_LOCAL")
|
||||||
|
map_api_key_speckle = os.environ.get("MAPTILER_KEY_SPECKLE")
|
||||||
|
|
||||||
|
global CONFIG
|
||||||
|
|
||||||
with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh:
|
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">© MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© 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">© MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© 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'© <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)
|
||||||
|
|||||||
+144
-6
@@ -34,8 +34,13 @@ import os
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from datetime import datetime, timezone
|
||||||
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
|
||||||
|
|
||||||
|
import json
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
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
|
||||||
@@ -45,9 +50,10 @@ import pygeoapi.api.maps as maps_api
|
|||||||
import pygeoapi.api.processes as processes_api
|
import pygeoapi.api.processes as processes_api
|
||||||
import pygeoapi.api.stac as stac_api
|
import pygeoapi.api.stac as stac_api
|
||||||
import pygeoapi.api.tiles as tiles_api
|
import pygeoapi.api.tiles as tiles_api
|
||||||
|
from pygeoapi.provider.speckle_utils.legal import COUNTRY_CODES
|
||||||
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 +156,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]
|
||||||
@@ -164,6 +173,62 @@ def execute_from_flask(api_function, request: Request, *args,
|
|||||||
return get_response((headers, status, content))
|
return get_response((headers, status, content))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_client(url_route: str):
|
||||||
|
|
||||||
|
# if called fromm the browser, Exceptions from this function will result in infinite load
|
||||||
|
agent = request.headers.get('User-Agent')
|
||||||
|
if request.environ.get('HTTP_X_FORWARDED_FOR') is None:
|
||||||
|
ip_address = request.environ['REMOTE_ADDR']
|
||||||
|
else:
|
||||||
|
ip_address = request.environ['HTTP_X_FORWARDED_FOR']
|
||||||
|
|
||||||
|
if agent is not None and "(https://www.checklyhq.com)" not in agent:
|
||||||
|
print(f"_______________________{datetime.now().astimezone(timezone.utc)} _URL access")
|
||||||
|
print(f"_Agent {url_route}: {agent}")
|
||||||
|
print(f"_IP Address: {ip_address}")
|
||||||
|
print(f"_Request URL: {request.url}")
|
||||||
|
|
||||||
|
request.url += f"&userAgent={agent}"
|
||||||
|
|
||||||
|
# by Agent:
|
||||||
|
if agent is not None and ("YaBrowser/" in agent or "yandex" in agent.lower()):
|
||||||
|
raise ValueError("Your browser is not supported.")
|
||||||
|
|
||||||
|
# by IP:
|
||||||
|
try:
|
||||||
|
url = 'https://ipinfo.io/' + ip_address + '/json'
|
||||||
|
res = urlopen(url)
|
||||||
|
data = json.load(res)
|
||||||
|
if isinstance(data, dict) and isinstance(data["country"], str):
|
||||||
|
if data["country"].lower() in COUNTRY_CODES:
|
||||||
|
raise PermissionError("Review Speckle Terms and Conditions")
|
||||||
|
else:
|
||||||
|
print(f"Error validating client: DATA {data}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error validating client from start: {e}")
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
collection_id = "speckle"
|
||||||
|
|
||||||
|
yield loading_screen().data
|
||||||
|
|
||||||
|
handle_client("/")
|
||||||
|
CONFIG = get_config(request=request)
|
||||||
|
api_ = API(CONFIG, OPENAPI)
|
||||||
|
|
||||||
|
try:
|
||||||
|
browser_response = execute_from_flask(itemtypes_api.get_collection_items,
|
||||||
|
request, collection_id,
|
||||||
|
skip_valid_check=True)
|
||||||
|
yield browser_response.data
|
||||||
|
|
||||||
|
except PermissionError as ex:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
yield error_screen(ex).data
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route('/')
|
@BLUEPRINT.route('/')
|
||||||
def landing_page():
|
def landing_page():
|
||||||
"""
|
"""
|
||||||
@@ -171,8 +236,45 @@ def landing_page():
|
|||||||
|
|
||||||
:returns: HTTP response
|
:returns: HTTP response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
agent = request.headers.get('User-Agent')
|
||||||
|
browser_agent = False
|
||||||
|
browser_list = ["Chrome", "Safari", "Firefox", "Edg/", "Trident/"]
|
||||||
|
for br in browser_list:
|
||||||
|
if agent is not None and 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
|
||||||
|
return Response(stream_with_context(generate()))
|
||||||
|
|
||||||
|
# for non-browsers
|
||||||
|
handle_client("/")
|
||||||
|
CONFIG = get_config(request=request)
|
||||||
|
api_ = API(CONFIG, OPENAPI)
|
||||||
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():
|
||||||
@@ -181,7 +283,8 @@ def openapi():
|
|||||||
|
|
||||||
:returns: HTTP response
|
:returns: HTTP response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# raise NotImplementedError()
|
||||||
return get_response(api_.openapi_(request))
|
return get_response(api_.openapi_(request))
|
||||||
|
|
||||||
|
|
||||||
@@ -192,7 +295,8 @@ def conformance():
|
|||||||
|
|
||||||
:returns: HTTP response
|
:returns: HTTP response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# raise NotImplementedError()
|
||||||
return get_response(api_.conformance(request))
|
return get_response(api_.conformance(request))
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +310,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 +323,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -231,10 +337,21 @@ def collections(collection_id=None):
|
|||||||
|
|
||||||
:returns: HTTP response
|
:returns: HTTP response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
handle_client("/collections")
|
||||||
return get_response(api_.describe_collections(request, collection_id))
|
return get_response(api_.describe_collections(request, collection_id))
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route('/speckle')
|
||||||
|
def speckle_collection():
|
||||||
|
|
||||||
|
handle_client("/speckle")
|
||||||
|
|
||||||
|
collection_id="speckle"
|
||||||
|
|
||||||
|
return collection_items(collection_id=collection_id)
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route('/collections/<path:collection_id>/schema')
|
@BLUEPRINT.route('/collections/<path:collection_id>/schema')
|
||||||
def collection_schema(collection_id):
|
def collection_schema(collection_id):
|
||||||
"""
|
"""
|
||||||
@@ -245,6 +362,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 +376,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 +397,10 @@ def collection_items(collection_id, item_id=None):
|
|||||||
|
|
||||||
:returns: HTTP response
|
:returns: HTTP response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
handle_client(f"/collections/{collection_id}/items")
|
||||||
|
|
||||||
|
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 +449,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 +464,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 +481,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 +503,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 +523,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 +540,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 +557,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 +578,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 +594,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 +610,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 +664,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 +678,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 +690,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 +709,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 +727,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))
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -0,0 +1,521 @@
|
|||||||
|
# =================================================================
|
||||||
|
#
|
||||||
|
# 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.project_name = ""
|
||||||
|
self.project_id = ""
|
||||||
|
self.model_name = ""
|
||||||
|
self.sourceApp = ""
|
||||||
|
|
||||||
|
self.crs = None
|
||||||
|
self.crs_dict = None
|
||||||
|
|
||||||
|
self.commit_gis = False
|
||||||
|
self.url_params = {"url_data_type":"", "url_preserve_attributes":"", "url_crs_authid":"", "url_lat":"","url_lon":"","url_north_degrees":"","url_limit":""}
|
||||||
|
self.times = {}
|
||||||
|
self.country_code = ""
|
||||||
|
|
||||||
|
self.requested_data_type: str = "polygons (default)" # points, lines, polygons, projectcomments
|
||||||
|
self.preserve_attributes: str = "true (default)"
|
||||||
|
self.lat: float = 48.76755913928929 #51.52486388756923
|
||||||
|
self.lon: float = 11.408741923664028 #0.1621445437168942
|
||||||
|
self.north_degrees: float = 0
|
||||||
|
self.crs_authid = ""
|
||||||
|
self.limit = 10000
|
||||||
|
self.user_agent = ""
|
||||||
|
|
||||||
|
self.missing_url = ""
|
||||||
|
self.limit_message = ""
|
||||||
|
|
||||||
|
self.extent = [-180,-90,180,90]
|
||||||
|
self.extent3d = [-180,-90,0,180,90,1000]
|
||||||
|
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) # possible ValueError
|
||||||
|
|
||||||
|
# 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]}
|
||||||
|
|
||||||
|
# add URL parameters
|
||||||
|
data['speckle_url'] = self.speckle_url
|
||||||
|
data['requested_data_type'] = self.requested_data_type
|
||||||
|
data['preserve_attributes'] = self.preserve_attributes
|
||||||
|
data['crs_authid'] = self.crs_authid
|
||||||
|
data['lat'] = self.lat
|
||||||
|
data['lon'] = self.lon
|
||||||
|
data['north_degrees'] = self.north_degrees
|
||||||
|
data['limit'] = self.limit
|
||||||
|
data['missing_url'] = self.missing_url
|
||||||
|
|
||||||
|
|
||||||
|
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 datetime import datetime, timezone
|
||||||
|
from pygeoapi.provider.speckle_utils.server_utils import get_stream_branch, get_client, get_comments, set_actions
|
||||||
|
|
||||||
|
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 stream is None:
|
||||||
|
raise ValueError(f"Project from URL '{url_proj}' not found")
|
||||||
|
if branch is None:
|
||||||
|
raise ValueError(f"Model '{wrapper.model_id}' of the project '{stream['name']}' not found")
|
||||||
|
|
||||||
|
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.project_id = wrapper.stream_id
|
||||||
|
self.project_name = stream['name']
|
||||||
|
self.model_name = branch['name']
|
||||||
|
|
||||||
|
commit = branch["commits"]["items"][0]
|
||||||
|
objId = commit["referencedObject"]
|
||||||
|
self.sourceApp = commit["sourceApplication"]
|
||||||
|
|
||||||
|
transport = ServerTransport(client=client, account=client.account, stream_id=wrapper.stream_id)
|
||||||
|
if transport == None:
|
||||||
|
raise SpeckleException("Transport not found")
|
||||||
|
|
||||||
|
# receive commit
|
||||||
|
set_actions(self, client)
|
||||||
|
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"_{datetime.now().astimezone(timezone.utc)} _Rendering model '{branch['name']}' of the project '{stream['name']}'")
|
||||||
|
speckle_data = self.traverse_data(commit_obj, comments)
|
||||||
|
|
||||||
|
set_actions(self, client, "GEO post-receive")
|
||||||
|
|
||||||
|
speckle_data["features"].extend(speckle_data["comments"])
|
||||||
|
speckle_data["comments"] = []
|
||||||
|
|
||||||
|
speckle_data["project_id"] = wrapper.stream_id
|
||||||
|
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["extent3d"] = self.extent3d
|
||||||
|
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 isDisplayable, 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 to keep traversing the object's "x" attribute "item" (both conditions need to be fulfilled)
|
||||||
|
# 1. if the item type is not in supported (convertible) types or is GIS VectorLayer
|
||||||
|
# 2. if the item's value is a list or a GH object
|
||||||
|
rule = TraversalRule(
|
||||||
|
[lambda _: True],
|
||||||
|
lambda x: [
|
||||||
|
item
|
||||||
|
for item in x.get_member_names()
|
||||||
|
if (x.speckle_type.split(":")[-1] not in supported_types or isinstance(x, VectorLayer))
|
||||||
|
and (isinstance(getattr(x, item, None), list) or (self.sourceApp is not None and "grasshopper" in self.sourceApp.lower() and x.speckle_type == "Base") )
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# for the context list, save the displayable objects and Layers (for getting CRS for now)
|
||||||
|
context_list = [x for x in GraphTraversal([rule]).traverse(commit_obj) if isDisplayable(x.current) or x.current.speckle_type.endswith("VectorLayer")]
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
time_operation = (time2-time1).total_seconds()
|
||||||
|
self.times["time_sort"] = time_operation
|
||||||
|
# print(f"Sorting time: {time_operation}")
|
||||||
|
|
||||||
|
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,477 @@
|
|||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
polygon_3d = False
|
||||||
|
|
||||||
|
for mesh in f_base["displayValue"]:
|
||||||
|
for i, coord in enumerate(mesh.vertices):
|
||||||
|
if i>60:
|
||||||
|
break
|
||||||
|
if i%3 !=0:
|
||||||
|
continue
|
||||||
|
elif coord != 0:
|
||||||
|
polygon_3d = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if polygon_3d is False:
|
||||||
|
for geom in f_base["geometry"]:
|
||||||
|
convert_polygon(geom, coords, coord_counts)
|
||||||
|
else:
|
||||||
|
for geom in f_base["displayValue"]:
|
||||||
|
convert_mesh_or_brep(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,149 @@
|
|||||||
|
|
||||||
|
import copy
|
||||||
|
import math
|
||||||
|
from typing import List
|
||||||
|
from pygeoapi.provider.speckle_utils.legal import COUNTRY_CODES, STATES, POSTCODES
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
time_operation = (time2-time1).total_seconds()
|
||||||
|
self.times["time_reproject"] = time_operation
|
||||||
|
|
||||||
|
validate_coords(self, flat_coords[0])
|
||||||
|
if len(flat_coords)>2:
|
||||||
|
validate_coords(self, flat_coords[len(flat_coords)-1])
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
new_list = []
|
||||||
|
for ind in range_coords_indices:
|
||||||
|
try:
|
||||||
|
new_list.append(local_flat_coords[ind])
|
||||||
|
except Exception as e: # corrupted geometry, ignore altogether
|
||||||
|
new_list = []
|
||||||
|
break
|
||||||
|
if len(new_list)>0:
|
||||||
|
poly_part.append(new_list)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
time_operation = (time3-time2).total_seconds()
|
||||||
|
self.times["time_reconstruct_geometry"] = time_operation
|
||||||
|
# print(f"Construct back geometry time: {time_operation}")
|
||||||
|
|
||||||
|
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]
|
||||||
|
all_z = [x[2] for x in transformed]
|
||||||
|
self.extent = [min(all_x), min(all_y), max(all_x), max(all_y)]
|
||||||
|
self.extent3d = [min(all_x), min(all_y), min(all_z), max(all_x), max(all_y), max(all_z)]
|
||||||
|
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
|
||||||
|
|
||||||
|
def validate_coords(self, coords):
|
||||||
|
from geopy.geocoders import Nominatim
|
||||||
|
country_code = ""
|
||||||
|
state = ""
|
||||||
|
postcode = ""
|
||||||
|
try:
|
||||||
|
geolocator = Nominatim(user_agent="specklePygeoapi")
|
||||||
|
coord = f"{coords[1]}, {coords[0]}"
|
||||||
|
location = geolocator.reverse(coord, exactly_one=True)
|
||||||
|
if location is not None:
|
||||||
|
address = location.raw['address']
|
||||||
|
country_code = address.get('country_code', '')
|
||||||
|
state = address.get('state', '')
|
||||||
|
postcode = address.get('postcode', '')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error validating project location: {e}")
|
||||||
|
self.country_code = country_code
|
||||||
|
|
||||||
|
if country_code in COUNTRY_CODES or state in STATES or postcode in POSTCODES:
|
||||||
|
print(f"Validating project location: blocked LAT LON {coords[1]}, {coords[0]}, {country_code}, {state}, {postcode}")
|
||||||
|
raise PermissionError("Review Speckle Terms and Conditions")
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
|
||||||
|
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 + [c.current for c in context_list]
|
||||||
|
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"])
|
||||||
|
self.commit_gis = True
|
||||||
|
|
||||||
|
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,480 @@
|
|||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
sub_meshes = []
|
||||||
|
for _ in item.faces:
|
||||||
|
if count < all_count:
|
||||||
|
faces = []
|
||||||
|
verts = []
|
||||||
|
colors = []
|
||||||
|
|
||||||
|
vert_num = item.faces[count]
|
||||||
|
if vert_num == 0:
|
||||||
|
vert_num = 3
|
||||||
|
elif vert_num == 1:
|
||||||
|
vert_num = 4
|
||||||
|
|
||||||
|
faces.append(vert_num)
|
||||||
|
faces.extend([ x for x in list(range(vert_num))])
|
||||||
|
|
||||||
|
try:
|
||||||
|
for ind in range(vert_num):
|
||||||
|
face_vert_index = count+1+ind
|
||||||
|
#print(face_vert_index)
|
||||||
|
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)
|
||||||
|
|
||||||
|
sub_meshes.append((mesh, item))
|
||||||
|
|
||||||
|
except IndexError: # corrupted mesh, drop altogether
|
||||||
|
sub_meshes = []
|
||||||
|
break
|
||||||
|
|
||||||
|
display_objs.extend(sub_meshes)
|
||||||
|
|
||||||
|
elif item is not None:
|
||||||
|
display_objs.append((item, item))
|
||||||
|
|
||||||
|
return display_objs
|
||||||
|
|
||||||
|
def isDisplayable(obj: "Base") -> bool:
|
||||||
|
|
||||||
|
if is_primitive(obj):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if obj.speckle_type.endswith("Feature"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
displayValue = None
|
||||||
|
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):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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, Polyline, 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, Polyline) 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 is_primitive(obj) -> bool:
|
||||||
|
"""Check if the object can be converted directly."""
|
||||||
|
|
||||||
|
from specklepy.objects.geometry import Polyline, Point, Line, Arc, Circle, Curve, Polycurve, Mesh, Brep
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(obj, Point) or
|
||||||
|
isinstance(obj, Line) or
|
||||||
|
isinstance(obj, Polyline) or
|
||||||
|
isinstance(obj, Arc) or
|
||||||
|
isinstance(obj, Circle) or
|
||||||
|
isinstance(obj, Curve) or
|
||||||
|
isinstance(obj, Mesh)
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
return item
|
||||||
|
|
||||||
|
mesh = Mesh.create(faces= faces, vertices=verts, colors=colors)
|
||||||
|
|
||||||
|
if isinstance(displayValForColor, List) and len(displayValForColor)>0:
|
||||||
|
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) or (item.parent is not None and isinstance(item.parent.current, VectorLayer)):
|
||||||
|
DEFAULT_COLOR = (255 << 24) + (10 << 16) + (132 << 8) + 255 # speckle blue, speckle_blue
|
||||||
|
break
|
||||||
|
|
||||||
|
def getAllParents(tc: "TraversalContext"):
|
||||||
|
|
||||||
|
all_tc = [tc]
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
parent = tc.parent
|
||||||
|
if parent:
|
||||||
|
all_tc.append(parent)
|
||||||
|
tc = parent
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
return all_tc
|
||||||
|
|
||||||
|
def assign_color(self: "SpeckleProvider", obj_display_tc: "TraversalContext", props: Dict) -> None:
|
||||||
|
"""Get and assign color to feature displayProperties."""
|
||||||
|
|
||||||
|
from specklepy.objects.geometry import Mesh, Brep
|
||||||
|
|
||||||
|
# initialize Speckle Blue color
|
||||||
|
color = DEFAULT_COLOR
|
||||||
|
opacity = None
|
||||||
|
|
||||||
|
obj_display = obj_display_tc.current
|
||||||
|
|
||||||
|
try:
|
||||||
|
# first, choose if get color from the parent obj or displayValue
|
||||||
|
if hasattr(obj_display, 'displayStyle') or hasattr(obj_display, '@displayStyle') or hasattr(obj_display, 'renderMaterial') or hasattr(obj_display, '@renderMaterial'):
|
||||||
|
obj_display = obj_display_tc.current
|
||||||
|
else:
|
||||||
|
# this option will be not very reliable:
|
||||||
|
# there could be different colors for diff displayValues in the list
|
||||||
|
if hasattr(obj_display, 'displayValue'):
|
||||||
|
try:
|
||||||
|
displayVal = obj_display['displayValue']
|
||||||
|
except:
|
||||||
|
displayVal = obj_display.displayValue
|
||||||
|
if isinstance(displayVal, list) and len(displayVal)>0:
|
||||||
|
obj_display = displayVal[0]
|
||||||
|
|
||||||
|
elif hasattr(obj_display, '@displayValue') and isinstance(obj_display['@displayValue'], list) and len(obj_display['@displayValue'])>0:
|
||||||
|
obj_display = obj_display['@displayValue'][0]
|
||||||
|
|
||||||
|
# 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'):
|
||||||
|
try:
|
||||||
|
renderMaterial = obj_display['renderMaterial']
|
||||||
|
except:
|
||||||
|
renderMaterial = obj_display.renderMaterial
|
||||||
|
color = renderMaterial['diffuse']
|
||||||
|
opacity = 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:
|
||||||
|
colors_number = 0
|
||||||
|
all_colors = []
|
||||||
|
for c in obj_display.colors:
|
||||||
|
if c not in all_colors:
|
||||||
|
colors_number += 1
|
||||||
|
all_colors.append(c)
|
||||||
|
|
||||||
|
if colors_number>1:
|
||||||
|
all_a = 0
|
||||||
|
all_r = 0
|
||||||
|
all_g = 0
|
||||||
|
all_b = 0
|
||||||
|
for col in all_colors:
|
||||||
|
a, r, g, b = get_r_g_b(col)
|
||||||
|
all_a += a
|
||||||
|
all_r += r
|
||||||
|
all_g += g
|
||||||
|
all_b += b
|
||||||
|
color = (
|
||||||
|
(int(all_a/len(obj_display.colors)) << 24) + (int(all_r/len(obj_display.colors)) << 16)
|
||||||
|
+ (int(all_g/len(obj_display.colors)) << 8) + int(all_b/len(obj_display.colors))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
color = obj_display.colors[0]
|
||||||
|
|
||||||
|
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})'
|
||||||
|
|
||||||
|
# if still not found, check proxies:
|
||||||
|
if color == DEFAULT_COLOR:
|
||||||
|
for tc in getAllParents(obj_display_tc):
|
||||||
|
|
||||||
|
try:
|
||||||
|
color = self.material_color_proxies[tc.current.applicationId]
|
||||||
|
props['color'] = color
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
color = self.material_color_proxies[obj_display.applicationId]
|
||||||
|
props['color'] = color
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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_tc: "TraversalContext") -> None:
|
||||||
|
"""Assign displayProperties to the feature."""
|
||||||
|
|
||||||
|
from specklepy.objects.geometry import Mesh, Brep
|
||||||
|
|
||||||
|
assign_color(self, obj_display_tc, 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,236 @@
|
|||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
from specklepy.objects.graph_traversal.traversal import TraversalContext
|
||||||
|
from specklepy.objects.other import Collection
|
||||||
|
|
||||||
|
# print(f"Creating features..")
|
||||||
|
time1 = datetime.now()
|
||||||
|
|
||||||
|
all_props = []
|
||||||
|
feature_count = 0
|
||||||
|
|
||||||
|
if self.requested_data_type != "projectcomments":
|
||||||
|
for item in context_list:
|
||||||
|
|
||||||
|
if item.current.speckle_type.endswith("Collection") or item.current.speckle_type.endswith("Layer") or item.current.speckle_type.endswith("Proxy"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
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
|
||||||
|
speckle_type = item.current.speckle_type
|
||||||
|
if ":" in speckle_type:
|
||||||
|
speckle_type = speckle_type.split(":")[-1]
|
||||||
|
|
||||||
|
feature: Dict = {
|
||||||
|
"type": "Feature",
|
||||||
|
#"bbox": [-180.0, -90.0, 180.0, 90.0], should not be in degrees
|
||||||
|
"geometry": {},
|
||||||
|
"displayProperties":{
|
||||||
|
"object_type": "geometry",
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": f_id,
|
||||||
|
"FID": f_fid,
|
||||||
|
"speckle_type": speckle_type,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
obj_get_color_tc = TraversalContext(obj_get_color, "", item)
|
||||||
|
|
||||||
|
assign_display_properties(self, feature, f_base, obj_get_color_tc)
|
||||||
|
feature["max_height"] = max([c[2] for c in coords])
|
||||||
|
feature["bbox"] = get_feature_bbox(coords)
|
||||||
|
data["features"].append(feature)
|
||||||
|
feature_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
list_of_display_obj = find_list_of_display_obj(f_base) # tuple
|
||||||
|
|
||||||
|
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], should not be in degrees
|
||||||
|
"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)
|
||||||
|
|
||||||
|
obj_get_color_tc = TraversalContext(obj_display, "", item)
|
||||||
|
|
||||||
|
assign_display_properties(self, feature_new, f_base, obj_get_color_tc)
|
||||||
|
feature_new["max_height"] = max([c[2] for c in coords])
|
||||||
|
feature_new["bbox"] = get_feature_bbox(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(f"No supported features of type '{self.requested_data_type}' found. Make sure correct type is requested by adding a URL parameter (e.g. '&dataType=points').")
|
||||||
|
|
||||||
|
time2 = datetime.now()
|
||||||
|
|
||||||
|
time_operation = (time2-time1).total_seconds()
|
||||||
|
self.times["time_creating_features"] = time_operation
|
||||||
|
# print(f"Creating features time: {time_operation}")
|
||||||
|
|
||||||
|
def get_feature_bbox(coords) -> List[float]:
|
||||||
|
"""Get min max coordinates of the feature."""
|
||||||
|
|
||||||
|
x0 = min([c[0] for c in coords])
|
||||||
|
x1 = max([c[0] for c in coords])
|
||||||
|
y0 = min([c[1] for c in coords])
|
||||||
|
y1 = max([c[1] for c in coords])
|
||||||
|
|
||||||
|
return [x0, y0, x1, y1]
|
||||||
|
|
||||||
|
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>   {item["text"]}<br>"
|
||||||
|
for img in item["attachments"]:
|
||||||
|
properties["text_html"] += f" <i>   '{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,3 @@
|
|||||||
|
COUNTRY_CODES = ["ru"]
|
||||||
|
STATES = ['Автономна Республіка Крим', 'Севастополь', 'Донецька область', 'Луганська область']
|
||||||
|
POSTCODES = [str(i) for i in range(95000,99999)]
|
||||||
@@ -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,439 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
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,187 @@
|
|||||||
|
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]
|
||||||
|
]
|
||||||
|
|
||||||
|
# save headers and assign them back later
|
||||||
|
headers = self.session.headers
|
||||||
|
self.session.headers.update(
|
||||||
|
{
|
||||||
|
"Accept": "text/plain",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
self.session.headers = headers # return previous headers
|
||||||
|
|
||||||
|
# iter through returned objects saving them as we go
|
||||||
|
target_transport.begin_write()
|
||||||
|
for line in lines:
|
||||||
|
if line:
|
||||||
|
hash, obj = line.split("\t")
|
||||||
|
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,234 @@
|
|||||||
|
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"]["selection"]
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_actions(self: "SpeckleProvider", client: "SpeckleClient", action: str = "GEO receive"):
|
||||||
|
from specklepy.logging.metrics import track
|
||||||
|
try:
|
||||||
|
full_dict = {**self.url_params, **self.times}
|
||||||
|
full_dict["GIS commit"] = self.commit_gis
|
||||||
|
full_dict["project_id"] = f"{self.project_id}"
|
||||||
|
full_dict["sourceHostApp"] = self.sourceApp
|
||||||
|
full_dict["model"] = f"{self.project_name}, {self.model_name}"
|
||||||
|
full_dict["time_TOTAL"] = sum([x[1] for x in self.times.items()])
|
||||||
|
full_dict["model_url"] = self.speckle_url
|
||||||
|
full_dict["model_country_code"] = self.country_code
|
||||||
|
track(action, client.account, full_dict)
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"_Cannot set action '{action}': {ex}")
|
||||||
|
pass
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def get_set_url_parameters(self: "SpeckleProvider"):
|
||||||
|
"""Parse and save URL parameters."""
|
||||||
|
|
||||||
|
from pygeoapi.provider.speckle_utils.crs_utils import create_crs_from_authid
|
||||||
|
|
||||||
|
crsauthid = False
|
||||||
|
|
||||||
|
if (isinstance(self.data, str)):
|
||||||
|
|
||||||
|
for item in self.data.lower().split("&"):
|
||||||
|
# if CRS authid is found, rest will be ignored
|
||||||
|
if "speckleurl=" in item:
|
||||||
|
try:
|
||||||
|
speckle_url = item.split("speckleurl=")[1]
|
||||||
|
if "/projects/" not in speckle_url or "/models/" not in speckle_url:
|
||||||
|
raise ValueError(f"Provide valid Speckle Model URL: {item}")
|
||||||
|
|
||||||
|
if speckle_url[-1] == "/":
|
||||||
|
speckle_url = speckle_url[:-1]
|
||||||
|
self.speckle_project_url = speckle_url.split("/models")[0]
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Provide valid Speckle Model URL: {item}")
|
||||||
|
|
||||||
|
elif "datatype=" in item:
|
||||||
|
try:
|
||||||
|
requested_data_type = item.split("datatype=")[1]
|
||||||
|
if requested_data_type in ["points", "lines", "polygons", "projectcomments"]:
|
||||||
|
self.requested_data_type = requested_data_type
|
||||||
|
self.url_params["url_data_type"] = requested_data_type
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Provide valid dataType parameter (points/lines/polygons/projectcomments): {item}")
|
||||||
|
|
||||||
|
elif "preserveattributes=" in item:
|
||||||
|
try:
|
||||||
|
preserve_attributes = item.split("preserveattributes=")[1]
|
||||||
|
if preserve_attributes in ["true", "false"]:
|
||||||
|
self.preserve_attributes = preserve_attributes
|
||||||
|
self.url_params["url_preserve_attributes"] = preserve_attributes
|
||||||
|
except:
|
||||||
|
ValueError(f"Provide valid preserverAttributes parameter (true/false): {item}")
|
||||||
|
|
||||||
|
elif "crsauthid=" in item:
|
||||||
|
crs_authid = item.split("crsauthid=")[1]
|
||||||
|
if isinstance(crs_authid, str) and len(crs_authid)>3:
|
||||||
|
crsauthid = True
|
||||||
|
self.crs_authid = crs_authid
|
||||||
|
self.url_params["url_crs_authid"] = crs_authid
|
||||||
|
|
||||||
|
elif "lat=" in item:
|
||||||
|
try:
|
||||||
|
lat = float(item.split("lat=")[1])
|
||||||
|
self.lat = lat
|
||||||
|
self.url_params["url_lat"] = lat
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Invalid Lat input, must be numeric: {item}")
|
||||||
|
elif "lon=" in item:
|
||||||
|
try:
|
||||||
|
lon = float(item.split("lon=")[1])
|
||||||
|
self.lon = lon
|
||||||
|
self.url_params["url_lon"] = lon
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Invalid Lon input, must be numeric: {item}")
|
||||||
|
elif "northdegrees=" in item:
|
||||||
|
try:
|
||||||
|
north_degrees = float(item.split("northdegrees=")[1])
|
||||||
|
self.north_degrees = north_degrees
|
||||||
|
self.url_params["url_north_degrees"] = north_degrees
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Invalid northDegrees input, must be numeric: {item}")
|
||||||
|
elif "limit=" in item:
|
||||||
|
try:
|
||||||
|
limit = int(item.split("limit=")[1])
|
||||||
|
if limit>0:
|
||||||
|
self.limit = limit
|
||||||
|
self.url_params["url_limit"] = limit
|
||||||
|
except:
|
||||||
|
ValueError(f"Invalid limit input, must be a positive integer: {item}")
|
||||||
|
|
||||||
|
elif "useragent=" in item:
|
||||||
|
try:
|
||||||
|
agent = item.split("useragent=")[1]
|
||||||
|
self.user_agent = agent
|
||||||
|
self.url_params["user_agent"] = agent
|
||||||
|
except:
|
||||||
|
ValueError(f"Invalid limit input, must be a positive integer: {item}")
|
||||||
|
|
||||||
|
|
||||||
|
if self.speckle_url == "-":
|
||||||
|
self.missing_url = "true"
|
||||||
|
|
||||||
|
# if CRS authid is found, rest will be ignored
|
||||||
|
if crsauthid:
|
||||||
|
self.lat = str(self.lat) + " (not applied)"
|
||||||
|
self.lon = str(self.lon) + " (not applied)"
|
||||||
|
self.north_degrees = 0 # default to 0: rotation ignored when AuthId is used #str(self.north_degrees) + " (not applied)"
|
||||||
|
|
||||||
|
# if CRS parameter present, create and assign CRS:
|
||||||
|
if len(self.crs_authid)>3:
|
||||||
|
create_crs_from_authid(self, self.crs_authid)
|
||||||
@@ -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.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
+118
-58
@@ -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="https://github.com/specklesystems/pygeoapi/blob/dev/pygeoapi/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,72 @@
|
|||||||
{% 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>
|
<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">
|
||||||
{% if config['server']['admin'] %}
|
> Geolocating your data
|
||||||
<li class="nav-item">
|
</a>
|
||||||
<a href="{{ config['server']['url'] }}/admin/config" class="nav-link" aria-current="page">{% trans %}Admin{% endtrans %}</a>
|
{% if (data["model"] and data["model"]!="") %}
|
||||||
</li>
|
<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">
|
||||||
{% endif %}
|
> {{data["project"]}} >
|
||||||
<!--
|
</a>
|
||||||
Add additional menu items here
|
<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">
|
||||||
<a href="https://pygeoapi.io" class="nav-link">About</a>
|
{{data["model"]}} {{data["limit_message"]}}
|
||||||
-->
|
</a>
|
||||||
</ul>
|
{% 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 style="max-height:fit-content;margin:0px;padding:0px;background-color: rgb(10,132,255);">
|
||||||
|
<p style="text-align: center; margin:0px;padding:5px;">
|
||||||
|
<a href = "https://docs.google.com/forms/d/e/1FAIpQLScKW2pkcWll3deXEwoV_G5ozLtuU06_prw8rf8HFuCk4tmOPQ/viewform?usp=sf_link"
|
||||||
|
style="color:rgb(255, 255, 255)" target="_blank">We would love to hear your feedback!</a>
|
||||||
|
</p>
|
||||||
|
</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
|
||||||
|
|||||||
@@ -1,179 +1,755 @@
|
|||||||
{% 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 data['speckle_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 not data['speckle_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_url'].split('/models')[0]}}/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">
|
||||||
<div class="col-sm-12 col-md-6" style="overflow-x: scroll;">
|
<p>{% trans %}No items{% endtrans %}</p>
|
||||||
{% set props = [] %}
|
</div>
|
||||||
<table class="table table-striped table-bordered">
|
{% endif %}
|
||||||
<thead>
|
</section>
|
||||||
<tr>
|
|
||||||
{% if data.get('uri_field') %}
|
|
||||||
{% set uri_field = data.uri_field %}
|
</details>
|
||||||
<th>{{ uri_field }}</th>
|
|
||||||
{% elif data.get('title_field') %}
|
<tr><p></p></tr>
|
||||||
{% 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>
|
|
||||||
{% 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 %}
|
|
||||||
<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);
|
try {
|
||||||
map.addLayer(new L.TileLayer(
|
document.getElementById("loading_screen").remove();
|
||||||
'{{ config['server']['map']['url'] }}', {
|
document.getElementById("loading_screen_band").remove();
|
||||||
maxZoom: 18,
|
}
|
||||||
attribution: '{{ config['server']['map']['attribution'] | safe }}'
|
catch(err) {}
|
||||||
}
|
|
||||||
));
|
|
||||||
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 %}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function split_polygons(geojson_data){
|
||||||
|
features = []
|
||||||
|
geojson_data.forEach((element, index) => {
|
||||||
|
if (element.geometry.type == "MultiPolygon"){
|
||||||
|
// mesh faces are stored as parts, so most buildings might have hundreds of parts with just 1 loop or 3-4pts
|
||||||
|
all_parts = element.geometry.coordinates;
|
||||||
|
|
||||||
|
// loops are usually simple (3-4 vertices), and usually only loop
|
||||||
|
for (polyPart of all_parts){
|
||||||
|
new_coordinates = [polyPart];
|
||||||
|
new_element = {"id": element.properties.id, "type":"Feature",
|
||||||
|
"geometry": {"type": "MultiPolygon", "coordinates": new_coordinates},
|
||||||
|
"properties": element.properties,
|
||||||
|
"displayProperties": element.displayProperties };
|
||||||
|
|
||||||
|
features.push(new_element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
features.push(element)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return features
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {{ data | to_json | safe }};
|
||||||
|
var geojson_data_original = {{ data['features'] | to_json | safe }};
|
||||||
|
|
||||||
|
// Leaflet 2d map
|
||||||
|
function initialize2d() {
|
||||||
|
var map = L.map('map2d', {zoomControl: false}).setView([45, 0], 2);
|
||||||
|
L.control.zoom({
|
||||||
|
position: 'topright'
|
||||||
|
}).addTo(map);
|
||||||
|
var tileLayer = new L.TileLayer(
|
||||||
|
'{{ config['server']['map']['url'] }}', {
|
||||||
|
maxZoom: 22,
|
||||||
|
minZoom: 12,
|
||||||
|
attribution: '{{ config['server']['map']['attribution'] | safe }} © Data: <a href="https://speckle.systems/">Speckle Systems</a>'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
geojson_data = split_polygons(geojson_data_original);
|
||||||
|
project_url = ""
|
||||||
|
try {
|
||||||
|
project_url = data['speckle_url'].split("/models")[0]
|
||||||
|
}
|
||||||
|
catch(err) {}
|
||||||
|
|
||||||
|
|
||||||
|
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 = 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 = 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]);
|
||||||
|
// load proper basemap for Speckle models; but only zoomed-out one for empty data
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bounds = group.getBounds();
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
|
||||||
|
tileLayer.addTo(map);
|
||||||
|
group.addTo(map);
|
||||||
|
}
|
||||||
|
catch (err){
|
||||||
|
tileLayer = new L.TileLayer(
|
||||||
|
'{{ config['server']['map']['url'] }}', {
|
||||||
|
maxZoom: 2,
|
||||||
|
minZoom: 2,
|
||||||
|
attribution: '{{ config['server']['map']['attribution'] | safe }} © Data: <a href="https://speckle.systems/">Speckle Systems</a>'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
tileLayer.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//map.addLayer(lines);
|
||||||
|
// 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")) {
|
||||||
|
|
||||||
|
polygon_all_parts = []
|
||||||
|
// iterate through Polygon Parts
|
||||||
|
for (let p = 0; p < coords.length; p++) {
|
||||||
|
|
||||||
|
// check orientation of each PolygonPart, if vertical - shift points slightly
|
||||||
|
polygon_part = [];
|
||||||
|
inner = false;
|
||||||
|
|
||||||
|
for (let c = 0; c < coords[p].length; c++) {
|
||||||
|
polygon_part_loop = [];
|
||||||
|
if (c>0){
|
||||||
|
inner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.000000001 < sum_orientation && sum_orientation <0.000000001){
|
||||||
|
|
||||||
|
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];
|
||||||
|
polygon_part_loop = multipolygon_coords;
|
||||||
|
|
||||||
|
polygon_part = [polygon_part_loop];
|
||||||
|
polygon_all_parts.push(polygon_part);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (polygon_pts.length==4) {
|
||||||
|
createdPolygon = true;
|
||||||
|
|
||||||
|
multipolygon_coords = coords[p][c].slice(0,3);
|
||||||
|
polygon_part_loop = multipolygon_coords;
|
||||||
|
|
||||||
|
polygon_part = [polygon_part_loop];
|
||||||
|
polygon_all_parts.push(polygon_part);
|
||||||
|
|
||||||
|
/////////
|
||||||
|
multipolygon_coords = [coords[p][c][2], coords[p][c][3], coords[p][c][0]];
|
||||||
|
polygon_part_loop = multipolygon_coords;
|
||||||
|
|
||||||
|
polygon_part = [polygon_part_loop];
|
||||||
|
polygon_all_parts.push(polygon_part);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
if (createdPolygon == false){ // if non-vertical, or vertical with more than 4 vertices
|
||||||
|
|
||||||
|
multipolygon_coords = coords[p][c];
|
||||||
|
polygon_part_loop = multipolygon_coords;
|
||||||
|
|
||||||
|
if (inner == false){
|
||||||
|
polygon_part = [polygon_part_loop];
|
||||||
|
polygon_all_parts.push(polygon_part);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
polygon_all_parts[polygon_all_parts.length-1].push(polygon_part_loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
new_polygon = {"id": speckle_features.length, "type":"Feature",
|
||||||
|
"geometry": {"type": "MultiPolygon", "coordinates":polygon_all_parts},
|
||||||
|
"properties": speckle_data.features[i].properties,
|
||||||
|
"displayProperties": speckle_data.features[i].displayProperties };
|
||||||
|
|
||||||
|
new_polygon.displayProperties.lineWidth = 0.05
|
||||||
|
speckle_features.push(new_polygon);
|
||||||
|
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
minZoom: 12,
|
||||||
|
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 %}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!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>
|
||||||
|
try {
|
||||||
|
document.getElementById("loading_screen").remove();
|
||||||
|
}
|
||||||
|
catch(err) {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -5,4 +5,10 @@
|
|||||||
<h2>{% trans %}Exception{% endtrans %}</h2>
|
<h2>{% trans %}Exception{% endtrans %}</h2>
|
||||||
<p>{{ data['description'] }}</p>
|
<p>{{ data['description'] }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
document.getElementById("loading_screen").remove();
|
||||||
|
}
|
||||||
|
catch(err) {}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="loading_screen_band" style="position:absolute;z-index:-1;width:100%;max-height:fit-content;margin:0px;padding:0px;background-color: rgb(10,132,255);">
|
||||||
|
<p style="text-align: center; margin:0px;padding:15px;">
|
||||||
|
<a href = "https://docs.google.com/forms/d/e/1FAIpQLScKW2pkcWll3deXEwoV_G5ozLtuU06_prw8rf8HFuCk4tmOPQ/viewform?usp=sf_link"
|
||||||
|
style="color:rgb(255, 255, 255)" target="_blank">Why not share a feedback while waiting?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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_winter.gif" alt="Loading data.." >
|
||||||
|
<h3 style="margin-bottom: 0px;text-align: center; color:rgb(40, 127, 209);font-size: x-large;">"I'm on my way!"</h3>
|
||||||
|
<p style="text-align: right; color:rgb(40, 127, 209)">- your data </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,6 +2,7 @@ Babel
|
|||||||
click
|
click
|
||||||
filelock
|
filelock
|
||||||
Flask
|
Flask
|
||||||
|
geopy==2.4.1
|
||||||
jinja2
|
jinja2
|
||||||
jsonschema
|
jsonschema
|
||||||
pydantic<2.0
|
pydantic<2.0
|
||||||
@@ -17,3 +18,4 @@ shapely
|
|||||||
SQLAlchemy<2.0.0
|
SQLAlchemy<2.0.0
|
||||||
tinydb
|
tinydb
|
||||||
unicodecsv
|
unicodecsv
|
||||||
|
specklepy==2.19.6
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Examples of adding Speckle layers to maps built with Leaflet and OpenLayers.
|
||||||
@@ -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: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> © 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/speckle/?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>
|
||||||
@@ -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: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> © Data: <a href="https://speckle.systems/">Speckle Systems</a>'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const speckle_data = await fetch('https://geo.speckle.systems/speckle/?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: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a> © Data: <a href="https://speckle.systems/">Speckle Systems</a>'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const speckle_data = await fetch('https://geo.speckle.systems/speckle/?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>
|
||||||
@@ -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: '© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const speckle_data2 = await fetch('https://geo.speckle.systems/speckle/?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(' ', ' ').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/speckle/?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(' ', ' ').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,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: '© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
//const speckle_data = await fetch('https://geo.speckle.systems/speckle/?speckleUrl=https://app.speckle.systems/projects/344f803f81/models/5582ab673e&datatype=projectcomments', {
|
||||||
|
//var speckle_url = 'http://localhost:5000/speckle/?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 = 'https://geo.speckle.systems/speckle/?speckleUrl=https://app.speckle.systems/projects/5feae56049/models/01c4183677&limit=1000000&datatype=polygons&preserveattributes=true';
|
||||||
|
|
||||||
|
//var speckle_url = 'https://geo.speckle.systems/speckle/?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(' ', ' ').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/speckle/?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: ' © 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>
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user