Browse Source
* add base map infrastructure * add leaflet css * add tencent map * add google maps support * added image map support * refactor schemes * here maps support && WIP on markers * add simple marker suppor * data update & polyline support * map bouds support * add some settings support * add map provider select to settings * labels support * WIP on trip animation widget * WIP on history control and route interpolation * trip-animation map provider & custom markers * comleted track marker & history controls * add license headers * label fix & tooltips support * WIP on polygons * marker dropping support * add polygon support * add label to trip animation * WIP on tooltips * lint anf typed leaflet AddMarker * some typing and poly improvements * add typing * add marker creation * update proxy * save position fix Co-authored-by: Artem Halushko <ahalushko@thingboards.io> Co-authored-by: Adsumus <artemtv42@gmail.com>pull/2540/head
committed by
GitHub
44 changed files with 3926 additions and 54 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,201 @@ |
|||||
|
Apache License |
||||
|
Version 2.0, January 2004 |
||||
|
http://www.apache.org/licenses/ |
||||
|
|
||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
|
||||
|
1. Definitions. |
||||
|
|
||||
|
"License" shall mean the terms and conditions for use, reproduction, |
||||
|
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
|
||||
|
"Licensor" shall mean the copyright owner or entity authorized by |
||||
|
the copyright owner that is granting the License. |
||||
|
|
||||
|
"Legal Entity" shall mean the union of the acting entity and all |
||||
|
other entities that control, are controlled by, or are under common |
||||
|
control with that entity. For the purposes of this definition, |
||||
|
"control" means (i) the power, direct or indirect, to cause the |
||||
|
direction or management of such entity, whether by contract or |
||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
|
||||
|
"You" (or "Your") shall mean an individual or Legal Entity |
||||
|
exercising permissions granted by this License. |
||||
|
|
||||
|
"Source" form shall mean the preferred form for making modifications, |
||||
|
including but not limited to software source code, documentation |
||||
|
source, and configuration files. |
||||
|
|
||||
|
"Object" form shall mean any form resulting from mechanical |
||||
|
transformation or translation of a Source form, including but |
||||
|
not limited to compiled object code, generated documentation, |
||||
|
and conversions to other media types. |
||||
|
|
||||
|
"Work" shall mean the work of authorship, whether in Source or |
||||
|
Object form, made available under the License, as indicated by a |
||||
|
copyright notice that is included in or attached to the work |
||||
|
(an example is provided in the Appendix below). |
||||
|
|
||||
|
"Derivative Works" shall mean any work, whether in Source or Object |
||||
|
form, that is based on (or derived from) the Work and for which the |
||||
|
editorial revisions, annotations, elaborations, or other modifications |
||||
|
represent, as a whole, an original work of authorship. For the purposes |
||||
|
of this License, Derivative Works shall not include works that remain |
||||
|
separable from, or merely link (or bind by name) to the interfaces of, |
||||
|
the Work and Derivative Works thereof. |
||||
|
|
||||
|
"Contribution" shall mean any work of authorship, including |
||||
|
the original version of the Work and any modifications or additions |
||||
|
to that Work or Derivative Works thereof, that is intentionally |
||||
|
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
|
or by an individual or Legal Entity authorized to submit on behalf of |
||||
|
the copyright owner. For the purposes of this definition, "submitted" |
||||
|
means any form of electronic, verbal, or written communication sent |
||||
|
to the Licensor or its representatives, including but not limited to |
||||
|
communication on electronic mailing lists, source code control systems, |
||||
|
and issue tracking systems that are managed by, or on behalf of, the |
||||
|
Licensor for the purpose of discussing and improving the Work, but |
||||
|
excluding communication that is conspicuously marked or otherwise |
||||
|
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
|
||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
|
on behalf of whom a Contribution has been received by Licensor and |
||||
|
subsequently incorporated within the Work. |
||||
|
|
||||
|
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
|
copyright license to reproduce, prepare Derivative Works of, |
||||
|
publicly display, publicly perform, sublicense, and distribute the |
||||
|
Work and such Derivative Works in Source or Object form. |
||||
|
|
||||
|
3. Grant of Patent License. Subject to the terms and conditions of |
||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
|
(except as stated in this section) patent license to make, have made, |
||||
|
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
|
where such license applies only to those patent claims licensable |
||||
|
by such Contributor that are necessarily infringed by their |
||||
|
Contribution(s) alone or by combination of their Contribution(s) |
||||
|
with the Work to which such Contribution(s) was submitted. If You |
||||
|
institute patent litigation against any entity (including a |
||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
|
or a Contribution incorporated within the Work constitutes direct |
||||
|
or contributory patent infringement, then any patent licenses |
||||
|
granted to You under this License for that Work shall terminate |
||||
|
as of the date such litigation is filed. |
||||
|
|
||||
|
4. Redistribution. You may reproduce and distribute copies of the |
||||
|
Work or Derivative Works thereof in any medium, with or without |
||||
|
modifications, and in Source or Object form, provided that You |
||||
|
meet the following conditions: |
||||
|
|
||||
|
(a) You must give any other recipients of the Work or |
||||
|
Derivative Works a copy of this License; and |
||||
|
|
||||
|
(b) You must cause any modified files to carry prominent notices |
||||
|
stating that You changed the files; and |
||||
|
|
||||
|
(c) You must retain, in the Source form of any Derivative Works |
||||
|
that You distribute, all copyright, patent, trademark, and |
||||
|
attribution notices from the Source form of the Work, |
||||
|
excluding those notices that do not pertain to any part of |
||||
|
the Derivative Works; and |
||||
|
|
||||
|
(d) If the Work includes a "NOTICE" text file as part of its |
||||
|
distribution, then any Derivative Works that You distribute must |
||||
|
include a readable copy of the attribution notices contained |
||||
|
within such NOTICE file, excluding those notices that do not |
||||
|
pertain to any part of the Derivative Works, in at least one |
||||
|
of the following places: within a NOTICE text file distributed |
||||
|
as part of the Derivative Works; within the Source form or |
||||
|
documentation, if provided along with the Derivative Works; or, |
||||
|
within a display generated by the Derivative Works, if and |
||||
|
wherever such third-party notices normally appear. The contents |
||||
|
of the NOTICE file are for informational purposes only and |
||||
|
do not modify the License. You may add Your own attribution |
||||
|
notices within Derivative Works that You distribute, alongside |
||||
|
or as an addendum to the NOTICE text from the Work, provided |
||||
|
that such additional attribution notices cannot be construed |
||||
|
as modifying the License. |
||||
|
|
||||
|
You may add Your own copyright statement to Your modifications and |
||||
|
may provide additional or different license terms and conditions |
||||
|
for use, reproduction, or distribution of Your modifications, or |
||||
|
for any such Derivative Works as a whole, provided Your use, |
||||
|
reproduction, and distribution of the Work otherwise complies with |
||||
|
the conditions stated in this License. |
||||
|
|
||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
|
any Contribution intentionally submitted for inclusion in the Work |
||||
|
by You to the Licensor shall be under the terms and conditions of |
||||
|
this License, without any additional terms or conditions. |
||||
|
Notwithstanding the above, nothing herein shall supersede or modify |
||||
|
the terms of any separate license agreement you may have executed |
||||
|
with Licensor regarding such Contributions. |
||||
|
|
||||
|
6. Trademarks. This License does not grant permission to use the trade |
||||
|
names, trademarks, service marks, or product names of the Licensor, |
||||
|
except as required for reasonable and customary use in describing the |
||||
|
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
|
||||
|
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
|
agreed to in writing, Licensor provides the Work (and each |
||||
|
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
|
implied, including, without limitation, any warranties or conditions |
||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
|
appropriateness of using or redistributing the Work and assume any |
||||
|
risks associated with Your exercise of permissions under this License. |
||||
|
|
||||
|
8. Limitation of Liability. In no event and under no legal theory, |
||||
|
whether in tort (including negligence), contract, or otherwise, |
||||
|
unless required by applicable law (such as deliberate and grossly |
||||
|
negligent acts) or agreed to in writing, shall any Contributor be |
||||
|
liable to You for damages, including any direct, indirect, special, |
||||
|
incidental, or consequential damages of any character arising as a |
||||
|
result of this License or out of the use or inability to use the |
||||
|
Work (including but not limited to damages for loss of goodwill, |
||||
|
work stoppage, computer failure or malfunction, or any and all |
||||
|
other commercial damages or losses), even if such Contributor |
||||
|
has been advised of the possibility of such damages. |
||||
|
|
||||
|
9. Accepting Warranty or Additional Liability. While redistributing |
||||
|
the Work or Derivative Works thereof, You may choose to offer, |
||||
|
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
|
or other liability obligations and/or rights consistent with this |
||||
|
License. However, in accepting such obligations, You may act only |
||||
|
on Your own behalf and on Your sole responsibility, not on behalf |
||||
|
of any other Contributor, and only if You agree to indemnify, |
||||
|
defend, and hold each Contributor harmless for any liability |
||||
|
incurred by, or claims asserted against, such Contributor by reason |
||||
|
of your accepting any such warranty or additional liability. |
||||
|
|
||||
|
END OF TERMS AND CONDITIONS |
||||
|
|
||||
|
APPENDIX: How to apply the Apache License to your work. |
||||
|
|
||||
|
To apply the Apache License to your work, attach the following |
||||
|
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
|
replaced with your own identifying information. (Don't include |
||||
|
the brackets!) The text should be enclosed in the appropriate |
||||
|
comment syntax for the file format. We also recommend that a |
||||
|
file or class name and description of purpose be included on the |
||||
|
same "printed page" as the copyright notice for easier |
||||
|
identification within third-party archives. |
||||
|
|
||||
|
Copyright [yyyy] [name of copyright owner] |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
@ -0,0 +1,83 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { JsonSchema, JsonSettingsSchema } from '@app/shared/public-api'; |
||||
|
|
||||
|
|
||||
|
export function initSchema(): JsonSettingsSchema { |
||||
|
return { |
||||
|
schema: { |
||||
|
type: 'object', |
||||
|
properties: {}, |
||||
|
required: [] |
||||
|
}, |
||||
|
form: [], |
||||
|
groupInfoes: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function addGroupInfo(schema: JsonSettingsSchema, title: string) { |
||||
|
schema.groupInfoes.push({ |
||||
|
formIndex: schema.groupInfoes?.length || 0, |
||||
|
GroupTitle: title |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function addToSchema(schema: JsonSettingsSchema, newSchema: JsonSettingsSchema) { |
||||
|
Object.assign(schema.schema.properties, newSchema.schema.properties); |
||||
|
schema.schema.required = schema.schema.required.concat(newSchema.schema.required); |
||||
|
schema.form.push(newSchema.form); |
||||
|
} |
||||
|
|
||||
|
export function mergeSchemes(schemes: JsonSettingsSchema[]): JsonSettingsSchema { |
||||
|
return schemes.reduce((finalSchema: JsonSettingsSchema, schema: JsonSettingsSchema) => { |
||||
|
return { |
||||
|
schema: { |
||||
|
properties: { |
||||
|
...finalSchema.schema.properties, |
||||
|
...schema.schema.properties |
||||
|
}, |
||||
|
required: [ |
||||
|
...finalSchema.schema.required, |
||||
|
...schema.schema.required |
||||
|
] |
||||
|
}, |
||||
|
form: [ |
||||
|
...finalSchema.form, |
||||
|
...schema.form |
||||
|
] |
||||
|
} as JsonSettingsSchema; |
||||
|
}, initSchema()); |
||||
|
} |
||||
|
|
||||
|
export function addCondition(schema: JsonSettingsSchema, condition: String): JsonSettingsSchema { |
||||
|
schema.form = schema.form.map(element => { |
||||
|
if (typeof element === 'string') { |
||||
|
return { |
||||
|
key: element, |
||||
|
condition |
||||
|
} |
||||
|
} |
||||
|
if (typeof element == 'object') { |
||||
|
if (element.condition) { |
||||
|
element.condition += ' && ' + condition |
||||
|
} |
||||
|
else element.condition = condition; |
||||
|
} |
||||
|
return element; |
||||
|
}); |
||||
|
return schema; |
||||
|
} |
||||
@ -0,0 +1,310 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
|
||||
|
import 'leaflet-providers'; |
||||
|
import 'leaflet.markercluster/dist/MarkerCluster.css' |
||||
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css' |
||||
|
import 'leaflet.markercluster/dist/leaflet.markercluster' |
||||
|
|
||||
|
import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings } from './map-models'; |
||||
|
import { Marker } from './markers'; |
||||
|
import { Observable, of, BehaviorSubject, Subject } from 'rxjs'; |
||||
|
import { filter } from 'rxjs/operators'; |
||||
|
import { Polyline } from './polyline'; |
||||
|
import { Polygon } from './polygon'; |
||||
|
|
||||
|
export default abstract class LeafletMap { |
||||
|
|
||||
|
markers: Map<string, Marker> = new Map(); |
||||
|
dragMode = true; |
||||
|
poly: Polyline; |
||||
|
polygon: Polygon; |
||||
|
map: L.Map; |
||||
|
map$: BehaviorSubject<L.Map> = new BehaviorSubject(null); |
||||
|
ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map)); |
||||
|
options: UnitedMapSettings; |
||||
|
isMarketCluster: boolean; |
||||
|
bounds: L.LatLngBounds; |
||||
|
newMarker: L.Marker; |
||||
|
datasources: FormattedData[]; |
||||
|
|
||||
|
constructor(public $container: HTMLElement, options: UnitedMapSettings) { |
||||
|
this.options = options; |
||||
|
} |
||||
|
|
||||
|
public initSettings(options: MapSettings) { |
||||
|
const { initCallback, |
||||
|
disableScrollZooming, }: MapSettings = options; |
||||
|
if (disableScrollZooming) { |
||||
|
this.map.scrollWheelZoom.disable(); |
||||
|
} |
||||
|
if (initCallback) { |
||||
|
setTimeout(options.initCallback, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addMarkerControl() { |
||||
|
if (this.options.draggableMarker) { |
||||
|
let mousePositionOnMap: L.LatLng; |
||||
|
let addMarker: L.Control; |
||||
|
this.map.on('mouseup', (e: L.LeafletMouseEvent) => { |
||||
|
mousePositionOnMap = e.latlng |
||||
|
}) |
||||
|
const dragListener = (e: L.DragEndEvent) => { |
||||
|
if (e.type === 'dragend' && mousePositionOnMap) { |
||||
|
const newMarker = L.marker(mousePositionOnMap).addTo(this.map); |
||||
|
const datasourcesList = document.createElement('div'); |
||||
|
const customLatLng = this.convertToCustomFormat(mousePositionOnMap); |
||||
|
this.datasources.forEach(ds => { |
||||
|
const dsItem = document.createElement('p'); |
||||
|
dsItem.appendChild(document.createTextNode(ds.entityName)); |
||||
|
dsItem.setAttribute('style', 'font-size: 14px'); |
||||
|
dsItem.onclick = () => { |
||||
|
const updatedEnttity = { ...ds, ...customLatLng }; |
||||
|
this.saveMarkerLocation(updatedEnttity); |
||||
|
this.map.removeLayer(newMarker); |
||||
|
this.deleteMarker(ds.aliasName); |
||||
|
this.createMarker(ds.aliasName, updatedEnttity, this.datasources, this.options, false); |
||||
|
} |
||||
|
datasourcesList.append(dsItem); |
||||
|
}) |
||||
|
const popup = L.popup(); |
||||
|
popup.setContent(datasourcesList); |
||||
|
newMarker.bindPopup(popup).openPopup(); |
||||
|
|
||||
|
} |
||||
|
addMarker.setPosition('topright') |
||||
|
} |
||||
|
L.Control.AddMarker = L.Control.extend({ |
||||
|
onAdd(map) { |
||||
|
const img = L.DomUtil.create('img') as any; |
||||
|
img.src = `assets/add_location.svg`; |
||||
|
img.style.width = '32px'; |
||||
|
img.style.height = '32px'; |
||||
|
img.onclick = this.dragMarker; |
||||
|
img.draggable = true; |
||||
|
const draggableImg = new L.Draggable(img); |
||||
|
draggableImg.enable(); |
||||
|
draggableImg.on('dragend', dragListener) |
||||
|
return img; |
||||
|
}, |
||||
|
onRemove(map) { |
||||
|
}, |
||||
|
dragMarker: this.dragMarker |
||||
|
|
||||
|
} as any); |
||||
|
|
||||
|
L.control.addMarker = (opts) => { |
||||
|
return new L.Control.AddMarker(opts); |
||||
|
} |
||||
|
|
||||
|
addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public setMap(map: L.Map) { |
||||
|
this.map = map; |
||||
|
if (this.options.useDefaultCenterPosition) { |
||||
|
this.map.panTo(this.options.defaultCenterPosition); |
||||
|
this.bounds = map.getBounds(); |
||||
|
} |
||||
|
else this.bounds = new L.LatLngBounds(null, null); |
||||
|
if (this.options.draggableMarker) { |
||||
|
this.addMarkerControl(); |
||||
|
} |
||||
|
this.map$.next(this.map); |
||||
|
} |
||||
|
|
||||
|
public setDataSources(dataSources) { |
||||
|
this.datasources = dataSources; |
||||
|
} |
||||
|
|
||||
|
public saveMarkerLocation(e) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
createLatLng(lat: number, lng: number): L.LatLng { |
||||
|
return L.latLng(lat, lng); |
||||
|
} |
||||
|
|
||||
|
createBounds(): L.LatLngBounds { |
||||
|
return this.map.getBounds(); |
||||
|
} |
||||
|
|
||||
|
extendBounds(bounds: L.LatLngBounds, polyline: L.Polyline) { |
||||
|
if (polyline && polyline.getLatLngs() && polyline.getBounds()) { |
||||
|
bounds.extend(polyline.getBounds()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
invalidateSize() { |
||||
|
this.map?.invalidateSize(true); |
||||
|
} |
||||
|
|
||||
|
onResize() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
getCenter() { |
||||
|
return this.map.getCenter(); |
||||
|
} |
||||
|
|
||||
|
convertPosition(expression: object): L.LatLng { |
||||
|
const lat = expression[this.options.latKeyName]; |
||||
|
const lng = expression[this.options.lngKeyName]; |
||||
|
if (isNaN(lat) || isNaN(lng)) |
||||
|
return null; |
||||
|
else |
||||
|
return L.latLng(lat, lng) as L.LatLng; |
||||
|
} |
||||
|
|
||||
|
convertToCustomFormat(position: L.LatLng): object { |
||||
|
return { |
||||
|
[this.options.latKeyName]: position.lat, |
||||
|
[this.options.lngKeyName]: position.lng |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Markers
|
||||
|
updateMarkers(markersData) { |
||||
|
markersData.forEach(data => { |
||||
|
if (this.convertPosition(data)) { |
||||
|
if (data.rotationAngle) { |
||||
|
this.options.icon = L.divIcon({ |
||||
|
html: `<div class="arrow" style="transform: rotate(${data.rotationAngle}deg)"><div>` |
||||
|
}) |
||||
|
} |
||||
|
else { |
||||
|
this.options.icon = null; |
||||
|
} |
||||
|
if (this.markers.get(data.aliasName)) { |
||||
|
this.updateMarker(data.aliasName, data, markersData, this.options) |
||||
|
} |
||||
|
else { |
||||
|
this.createMarker(data.aliasName, data, markersData, this.options as MarkerSettings); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
dragMarker = (e, data?) => { |
||||
|
if (e.type !== 'dragend') return; |
||||
|
this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) }); |
||||
|
} |
||||
|
|
||||
|
private createMarker(key, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) { |
||||
|
this.ready$.subscribe(() => { |
||||
|
const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker); |
||||
|
if (setFocus) |
||||
|
this.map.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); |
||||
|
this.markers.set(key, newMarker); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateMarker(key, data, dataSources, settings: MarkerSettings) { |
||||
|
const marker: Marker = this.markers.get(key); |
||||
|
const location = this.convertPosition(data) |
||||
|
if (!location.equals(marker.location)) { |
||||
|
marker.updateMarkerPosition(location); |
||||
|
} |
||||
|
if (settings.showTooltip) { |
||||
|
marker.updateMarkerTooltip(data); |
||||
|
} |
||||
|
marker.setDataSources(data, dataSources); |
||||
|
marker.updateMarkerIcon(settings); |
||||
|
} |
||||
|
|
||||
|
deleteMarker(key) { |
||||
|
let marker = this.markers.get(key)?.leafletMarker; |
||||
|
if (marker) { |
||||
|
this.map.removeLayer(marker); |
||||
|
this.markers.delete(key); |
||||
|
marker = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Polyline
|
||||
|
|
||||
|
updatePolylines(polyData: Array<Array<any>>) { |
||||
|
polyData.forEach(data => { |
||||
|
if (data.length) { |
||||
|
const dataSource = polyData.map(arr => arr[0]); |
||||
|
if (this.poly) { |
||||
|
this.updatePolyline(data, dataSource, this.options); |
||||
|
} |
||||
|
else { |
||||
|
this.createPolyline(data, dataSource, this.options); |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
createPolyline(data: any[], dataSources, settings) { |
||||
|
if (data.length) |
||||
|
this.ready$.subscribe(() => { |
||||
|
this.poly = new Polyline(this.map, |
||||
|
data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
||||
|
const bounds = this.bounds.extend(this.poly.leafletPoly.getBounds()); |
||||
|
if (bounds.isValid()) { |
||||
|
this.map.fitBounds(bounds); |
||||
|
this.bounds = bounds; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updatePolyline(data, dataSources, settings) { |
||||
|
this.ready$.subscribe(() => { |
||||
|
this.poly.updatePolyline(settings, data, dataSources); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Polygon
|
||||
|
|
||||
|
updatePolygons(polyData: any[]) { |
||||
|
polyData.forEach((data: any) => { |
||||
|
if (data.data.length && data.dataKey.name === this.options.polygonKeyName) { |
||||
|
if (typeof (data?.data[0][1]) === 'string') { |
||||
|
data.data = JSON.parse(data.data[0][1]); |
||||
|
} |
||||
|
if (this.polygon) { |
||||
|
this.updatePolygon(data.data, polyData, this.options); |
||||
|
} |
||||
|
else { |
||||
|
this.createPolygon(data.data, polyData, this.options); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
createPolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) { |
||||
|
this.ready$.subscribe(() => { |
||||
|
this.polygon = new Polygon(this.map, data, dataSources, settings); |
||||
|
const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds()); |
||||
|
if (bounds.isValid()) { |
||||
|
this.map.fitBounds(bounds); |
||||
|
this.bounds = bounds; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updatePolygon(data, dataSources, settings) { |
||||
|
this.ready$.subscribe(() => { |
||||
|
// this.polygon.updatePolygon(settings, data, dataSources);
|
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License; Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing; software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS;
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND; either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { LatLngTuple } from 'leaflet'; |
||||
|
|
||||
|
export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; |
||||
|
export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; |
||||
|
|
||||
|
export type MapSettings = { |
||||
|
polygonKeyName: any; |
||||
|
draggableMarker: boolean; |
||||
|
initCallback?: () => any; |
||||
|
defaultZoomLevel?: number; |
||||
|
dontFitMapBounds?: boolean; |
||||
|
disableScrollZooming?: boolean; |
||||
|
minZoomLevel?: number; |
||||
|
latKeyName?: string; |
||||
|
lngKeyName?: string; |
||||
|
xPosKeyName?: string; |
||||
|
yPosKeyName?: string; |
||||
|
mapProvider: MapProviders; |
||||
|
mapUrl?: string; |
||||
|
mapImageUrl?: string; |
||||
|
provider?: MapProviders; |
||||
|
credentials?: any; // declare credentials format
|
||||
|
defaultCenterPosition?: LatLngTuple; |
||||
|
markerClusteringSetting?; |
||||
|
useDefaultCenterPosition?: boolean; |
||||
|
gmDefaultMapType?: string; |
||||
|
useLabelFunction: string; |
||||
|
icon?: any; |
||||
|
} |
||||
|
|
||||
|
export enum MapProviders { |
||||
|
google = 'google-map', |
||||
|
openstreet = 'openstreet-map', |
||||
|
here = 'here', |
||||
|
image = 'image-map', |
||||
|
tencent = 'tencent-map' |
||||
|
} |
||||
|
|
||||
|
export type MarkerSettings = { |
||||
|
tooltipPattern?: any; |
||||
|
icon?: any; |
||||
|
showLabel?: boolean; |
||||
|
label: string; |
||||
|
labelColor: string; |
||||
|
labelText: string; |
||||
|
useLabelFunction: boolean; |
||||
|
draggableMarker: boolean; |
||||
|
showTooltip?: boolean; |
||||
|
color?: string; |
||||
|
autocloseTooltip: boolean; |
||||
|
displayTooltipAction: string; |
||||
|
currentImage?: string; |
||||
|
useMarkerImageFunction?: boolean; |
||||
|
markerImages?: string[]; |
||||
|
useMarkerImage: boolean; |
||||
|
markerImageSize: number; |
||||
|
markerImage: { |
||||
|
length: number |
||||
|
} |
||||
|
|
||||
|
colorFunction: GenericFunction; |
||||
|
tooltipFunction: GenericFunction; |
||||
|
labelFunction: GenericFunction; |
||||
|
markerImageFunction?: MarkerImageFunction; |
||||
|
} |
||||
|
|
||||
|
export interface FormattedData { |
||||
|
aliasName: string; |
||||
|
entityName: string; |
||||
|
$datasource: string; |
||||
|
dsIndex: number; |
||||
|
deviceType: string |
||||
|
} |
||||
|
|
||||
|
export type PolygonSettings = { |
||||
|
showPolygon: boolean; |
||||
|
showTooltip: any; |
||||
|
polygonStrokeOpacity: number; |
||||
|
polygonOpacity: number; |
||||
|
polygonStrokeWeight: number; |
||||
|
polygonStrokeColor: string; |
||||
|
polygonColor: string; |
||||
|
autocloseTooltip: boolean; |
||||
|
displayTooltipAction: string; |
||||
|
|
||||
|
polygonColorFunction?: GenericFunction; |
||||
|
} |
||||
|
|
||||
|
export type PolylineSettings = { |
||||
|
usePolylineDecorator: any; |
||||
|
autocloseTooltip: boolean; |
||||
|
displayTooltipAction: string; |
||||
|
useColorFunction: any; |
||||
|
color: string; |
||||
|
useStrokeOpacityFunction: any; |
||||
|
strokeOpacity: number; |
||||
|
useStrokeWeightFunction: any; |
||||
|
strokeWeight: number; |
||||
|
decoratorOffset: string | number; |
||||
|
endDecoratorOffset: string | number; |
||||
|
decoratorRepeat: string | number; |
||||
|
decoratorSymbol: any; |
||||
|
decoratorSymbolSize: any; |
||||
|
useDecoratorCustomColor: any; |
||||
|
decoratorCustomColor: any; |
||||
|
|
||||
|
|
||||
|
colorFunction: GenericFunction; |
||||
|
strokeOpacityFunction: GenericFunction; |
||||
|
strokeWeightFunction: GenericFunction; |
||||
|
} |
||||
|
|
||||
|
export interface HistorySelectSettings { |
||||
|
buttonColor: string; |
||||
|
} |
||||
|
|
||||
|
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolygonSettings; |
||||
@ -0,0 +1,31 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { JsonSettingsSchema } from '@app/shared/public-api'; |
||||
|
|
||||
|
export interface MapWidgetInterface { |
||||
|
resize(), |
||||
|
update(), |
||||
|
onInit(), |
||||
|
onDestroy(); |
||||
|
} |
||||
|
|
||||
|
export interface MapWidgetStaticInterface { |
||||
|
settingsSchema(mapProvider?, drawRoutes?): JsonSettingsSchema; |
||||
|
getProvidersSchema():JsonSettingsSchema |
||||
|
dataKeySettingsSchema(): object; |
||||
|
actionSources(): object; |
||||
|
} |
||||
@ -0,0 +1,258 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { MapProviders, UnitedMapSettings } from './map-models'; |
||||
|
import LeafletMap from './leaflet-map'; |
||||
|
import { |
||||
|
openstreetMapSettingsSchema, |
||||
|
googleMapSettingsSchema, |
||||
|
imageMapSettingsSchema, |
||||
|
tencentMapSettingsSchema, |
||||
|
commonMapSettingsSchema, |
||||
|
routeMapSettingsSchema, |
||||
|
markerClusteringSettingsSchema, |
||||
|
markerClusteringSettingsSchemaLeaflet, |
||||
|
hereMapSettingsSchema, |
||||
|
mapProviderSchema |
||||
|
} from './schemes'; |
||||
|
import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface'; |
||||
|
import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers'; |
||||
|
import { parseFunction, parseArray, parseData } from '@app/core/utils'; |
||||
|
import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@app/core/schema-utils'; |
||||
|
import { AttributeScope, EntityId, JsonSettingsSchema } from '@app/shared/public-api'; |
||||
|
import { forkJoin } from 'rxjs'; |
||||
|
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; |
||||
|
import { AttributeService } from '@app/core/public-api'; |
||||
|
import { getDefCenterPosition } from './maps-utils'; |
||||
|
|
||||
|
let providerSets; |
||||
|
let defaultSettings; |
||||
|
|
||||
|
export class MapWidgetController implements MapWidgetInterface { |
||||
|
|
||||
|
constructor(public mapProvider: MapProviders, private drawRoutes: boolean, public ctx: WidgetContext, $element: HTMLElement) { |
||||
|
if (this.map) { |
||||
|
this.map.map.remove(); |
||||
|
delete this.map; |
||||
|
} |
||||
|
|
||||
|
this.data = ctx.data; |
||||
|
if (!$element) { |
||||
|
$element = ctx.$container[0]; |
||||
|
} |
||||
|
this.settings = this.initSettings(ctx.settings); |
||||
|
|
||||
|
const MapClass = providerSets[this.provider]?.MapClass; |
||||
|
if (!MapClass) { |
||||
|
return; |
||||
|
} |
||||
|
this.map = new MapClass($element, this.settings); |
||||
|
this.map.saveMarkerLocation = this.setMarkerLocation; |
||||
|
} |
||||
|
|
||||
|
map: LeafletMap; |
||||
|
provider: MapProviders; |
||||
|
schema: JsonSettingsSchema; |
||||
|
data; |
||||
|
settings: UnitedMapSettings; |
||||
|
|
||||
|
public static dataKeySettingsSchema(): object { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
public static getProvidersSchema() { |
||||
|
return mergeSchemes([mapProviderSchema, |
||||
|
...Object.values(providerSets)?.map( |
||||
|
(setting: IProvider) => addCondition(setting?.schema, `model.provider === '${setting.name}'`))]); |
||||
|
} |
||||
|
|
||||
|
public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { |
||||
|
const schema = initSchema(); |
||||
|
addToSchema(schema, this.getProvidersSchema()); |
||||
|
addGroupInfo(schema, 'Map Provider Settings'); |
||||
|
addToSchema(schema, commonMapSettingsSchema); |
||||
|
addGroupInfo(schema, 'Common Map Settings'); |
||||
|
|
||||
|
if (drawRoutes) { |
||||
|
addToSchema(schema, routeMapSettingsSchema); |
||||
|
addGroupInfo(schema, 'Route Map Settings'); |
||||
|
} else if (mapProvider !== 'image-map') { |
||||
|
const clusteringSchema = mergeSchemes([markerClusteringSettingsSchemaLeaflet, markerClusteringSettingsSchema]) |
||||
|
addToSchema(schema, clusteringSchema); |
||||
|
addGroupInfo(schema, 'Markers Clustering Settings'); |
||||
|
} |
||||
|
return schema; |
||||
|
} |
||||
|
|
||||
|
public static actionSources(): object { |
||||
|
return { |
||||
|
markerClick: { |
||||
|
name: 'widget-action.marker-click', |
||||
|
multiple: false |
||||
|
}, |
||||
|
polygonClick: { |
||||
|
name: 'widget-action.polygon-click', |
||||
|
multiple: false |
||||
|
}, |
||||
|
tooltipAction: { |
||||
|
name: 'widget-action.tooltip-tag-action', |
||||
|
multiple: true |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
onInit() { |
||||
|
} |
||||
|
|
||||
|
setMarkerLocation = (e) => { |
||||
|
const attributeService = this.ctx.$injector.get(AttributeService); |
||||
|
forkJoin( |
||||
|
this.data.filter(data => !!e[data.dataKey.name]) |
||||
|
.map(data => { |
||||
|
const entityId: EntityId = { |
||||
|
entityType: data.datasource.entityType, |
||||
|
id: data.datasource.entityId |
||||
|
}; |
||||
|
return attributeService.saveEntityAttributes( |
||||
|
entityId, |
||||
|
AttributeScope.SHARED_SCOPE, |
||||
|
[{ |
||||
|
key: data.dataKey.name, |
||||
|
value: e[data.dataKey.name] |
||||
|
}] |
||||
|
); |
||||
|
})).subscribe(res => { |
||||
|
console.log('MapWidgetController -> setMarkerLocation -> res', res) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
initSettings(settings: UnitedMapSettings) { |
||||
|
const functionParams = ['data', 'dsData', 'dsIndex']; |
||||
|
this.provider = settings.provider || this.mapProvider; |
||||
|
const customOptions = { |
||||
|
provider: this.provider, |
||||
|
mapUrl: settings?.mapImageUrl, |
||||
|
labelFunction: parseFunction(settings.labelFunction, functionParams), |
||||
|
tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), |
||||
|
colorFunction: parseFunction(settings.colorFunction, functionParams), |
||||
|
polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), |
||||
|
markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), |
||||
|
labelColor: this.ctx.widgetConfig.color, |
||||
|
tooltipPattern: settings.tooltipPattern || |
||||
|
'<b>${entityName}</b><br/><br/><b>Latitude:</b> ${' + |
||||
|
settings.latKeyName + ':7}<br/><b>Longitude:</b> ${' + settings.lngKeyName + ':7}', |
||||
|
defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition), |
||||
|
currentImage: (settings.useMarkerImage && settings.markerImage?.length) ? { |
||||
|
url: settings.markerImage, |
||||
|
size: settings.markerImageSize || 34 |
||||
|
} : null |
||||
|
} |
||||
|
return { ...defaultSettings, ...settings, ...customOptions, } |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
if (this.drawRoutes) |
||||
|
this.map.updatePolylines(parseArray(this.data)); |
||||
|
if (this.settings.showPolygon) { |
||||
|
this.map.updatePolygons(this.data); |
||||
|
} |
||||
|
if (this.settings.draggableMarker) { |
||||
|
this.map.setDataSources(parseData(this.data)); |
||||
|
} |
||||
|
else |
||||
|
this.map.updateMarkers(parseData(this.data)); |
||||
|
} |
||||
|
|
||||
|
resize() { |
||||
|
this.map?.invalidateSize(); |
||||
|
this.map.onResize(); |
||||
|
} |
||||
|
|
||||
|
onDestroy() { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; |
||||
|
|
||||
|
interface IProvider { |
||||
|
MapClass: LeafletMap, |
||||
|
schema: JsonSettingsSchema, |
||||
|
name: string |
||||
|
} |
||||
|
|
||||
|
providerSets = { |
||||
|
'openstreet-map': { |
||||
|
MapClass: OpenStreetMap, |
||||
|
schema: openstreetMapSettingsSchema, |
||||
|
name: 'openstreet-map', |
||||
|
}, |
||||
|
'tencent-map': { |
||||
|
MapClass: TencentMap, |
||||
|
schema: tencentMapSettingsSchema, |
||||
|
name: 'tencent-map' |
||||
|
}, |
||||
|
'google-map': { |
||||
|
MapClass: GoogleMap, |
||||
|
schema: googleMapSettingsSchema, |
||||
|
name: 'google-map' |
||||
|
}, |
||||
|
here: { |
||||
|
MapClass: HEREMap, |
||||
|
schema: hereMapSettingsSchema, |
||||
|
name: 'here' |
||||
|
}, |
||||
|
'image-map': { |
||||
|
MapClass: ImageMap, |
||||
|
schema: imageMapSettingsSchema, |
||||
|
name: 'image-map' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
defaultSettings = { |
||||
|
xPosKeyName: 'xPos', |
||||
|
yPosKeyName: 'yPos', |
||||
|
markerOffsetX: 0.5, |
||||
|
markerOffsetY: 1, |
||||
|
latKeyName: 'latitude', |
||||
|
lngKeyName: 'longitude', |
||||
|
polygonKeyName: 'coordinates', |
||||
|
showLabel: false, |
||||
|
label: '${entityName}', |
||||
|
showTooltip: false, |
||||
|
useDefaultCenterPosition: false, |
||||
|
showTooltipAction: 'click', |
||||
|
autocloseTooltip: false, |
||||
|
showPolygon: true, |
||||
|
labelColor: '#000000', |
||||
|
color: '#FE7569', |
||||
|
polygonColor: '#0000ff', |
||||
|
polygonStrokeColor: '#fe0001', |
||||
|
polygonOpacity: 0.5, |
||||
|
polygonStrokeOpacity: 1, |
||||
|
polygonStrokeWeight: 1, |
||||
|
useLabelFunction: false, |
||||
|
markerImages: [], |
||||
|
strokeWeight: 2, |
||||
|
strokeOpacity: 1.0, |
||||
|
initCallback: () => { }, |
||||
|
defaultZoomLevel: 8, |
||||
|
dontFitMapBounds: false, |
||||
|
disableScrollZooming: false, |
||||
|
minZoomLevel: 16, |
||||
|
credentials: '', |
||||
|
markerClusteringSetting: null, |
||||
|
draggableMarker: false |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import _ from 'lodash'; |
||||
|
import { MarkerSettings, PolylineSettings, PolygonSettings } from './map-models'; |
||||
|
|
||||
|
export function createTooltip(target: L.Layer, settings: MarkerSettings | PolylineSettings | PolygonSettings): L.Popup { |
||||
|
const popup = L.popup(); |
||||
|
popup.setContent(''); |
||||
|
target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false }); |
||||
|
if (settings.displayTooltipAction === 'hover') { |
||||
|
target.off('click'); |
||||
|
target.on('mouseover', function () { |
||||
|
this.openPopup(); |
||||
|
}); |
||||
|
target.on('mouseout', function () { |
||||
|
this.closePopup(); |
||||
|
}); |
||||
|
} |
||||
|
return popup; |
||||
|
} |
||||
|
|
||||
|
export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { |
||||
|
return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); |
||||
|
}; |
||||
|
|
||||
|
export function findAngle(startPoint, endPoint) { |
||||
|
let angle = -Math.atan2(endPoint.latitude - startPoint.latitude, endPoint.longitude - startPoint.longitude); |
||||
|
angle = angle * 180 / Math.PI; |
||||
|
return parseInt(angle.toFixed(2), 10); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export function getDefCenterPosition(position) { |
||||
|
if (typeof (position) === 'string') |
||||
|
return position.split(','); |
||||
|
if (typeof (position) === 'object') |
||||
|
return position; |
||||
|
return [0, 0]; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
.arrow { |
||||
|
height: 30px; |
||||
|
width: 30px; |
||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAtCAMAAAANxBKoAAACAVBMVEUAAAAAqv9AgP8ggP8udP8qgP8ndv8kbf8kgP8eeP8rcf8mc/8jdP8hev8me/8kdv8jcv8pcP8ic/8neP8leP8ofP8mef8pev8nef8oe/8nef8pef8qfv8qgf8qe/8mev8off8oev8oef8oe/8pev8qe/8oe/8off8pfP8oev8pev8qe/8qfP8pev8pe/8qfP8pe/8oev8pe/8qe/8qfP8qfP8qff8qff8qfP8qff8pe/8qff8qfP8qfP8pev8pev8JYf8KYP8UY/8UZf8WY/8WZv8YZP8bZP8fZv8fZ/8gZv8gZ/8hZP8iZ/8jaf8jav8kaP8kav8ka/8kbP8kbf8kb/8lbP8lbf8lbv8lb/8lcP8mb/8mcP8mcf8ncf8ncv8nc/8ndP8ndf8ocv4odP8odf8odv8od/8oeP8peP8pef8pev8qev8qe/8qfP8rbP8rfP8rff8tc/4vcP4xbv4zcf42dP49df4+gP4/fv5Rif5biP1ul/14mvx4nvyMrfuMrvuWsfuctvuguPuww/vD0fnH1vrI1frK2PrL2fnV3vnb4vnc5fnd5fnd5vne5fnj6vnl6/jm7Pno7fXo7fbo7ffo7fjp7fnp7vjq7vjs8fnt8Pjt8fjw8fXw8fbw8fjy8/jz9fn5+fj7+/f++/j/+/X/+/b/+/f/+/j//Pj//vhoUaHMAAAAQHRSTlMAAwQICwwNDg4REhQWFxscHTI1QkRGSktOU1RdYWFscXJzhYWKqre6wMHDw8PHx8rN09TW1uLi7e/x8vLz9fj5hfwW0QAAAnVJREFUSMeV1fdb00AYB/BzoQjurSjuvQcuFLcgWqTVtLVpL0GS2GuDuLeouPeAiLOuwOFf6WU0ucvq4/d52v6Qz/M+18u97wHgyfh5m5vUYlHt2rV8BojOqGUqonNhVgTegfxZE2JXoMAUFgXYsSoKy3YfnoIiUqxm8UwUnSoaT0WVUuPi6lCUz+ft/+pqNYwqsiwrlm8s45XBVpE7BQgFyeZLbB1ihVwmefjpDSjbi7HwziArCdlU/MArjDHfaestBh4dZs9/JBb/4kVbo5FEr/JaWRL4VPvZ39jMVV4q63qiCx7bIfDJtst/LYt/JnLldaNuACbS9JwiQj4Zu/3HlDr5nEyJivMcgPn0Nogwfaq1Z9igQ7pO9AuqNEJzQINTV5ZghjvywLFG6dYMVRptAF3uPvBc8xPsWH0Q44ccpEqjvaBoa0lI7/ts2kHdCsbDJ5jSSC2/SEU8856x+hDGd+JZujQquvp6ieT7Nyel0o/jCSgzTefq0y81I30f7PRr2i1fbWrd+98yXtMGfOt29wTyiebHpu83dZ+m3U969qSBeje5DHeod8Dx5PeYZ7/rPO8y0XLvk+1J8UcJyLzLScw5kTsgz8Xu2vXJV0uaOSfsGcznZZGcwdi1L5qV13HmDILV3tY1z3fbxa8Wv+Se74VEj/H3jmz0TvuVN4Z+x/ZORF8efU64s/CtUT0vCbk0d/DZTSgxPQ/WhswTSYTZrDNPllaaVQo1q/Y4k63mv+YgmFZxxk6gR/LsCngcO+6nR9lCrfcqqQ2/dxqD7rT1IXhxyB24O8BuCr9eq9axU7F7wYjoy3ty3TZVLaCC2rRxru/hPwVgtEdFK5vOAAAAAElFTkSuQmCC"); |
||||
|
background-size: cover; |
||||
|
background-repeat: no-repeat; |
||||
|
background-position: center center; |
||||
|
} |
||||
|
|
||||
|
.leaflet-div-icon, |
||||
|
.tb-marker-label, |
||||
|
.tb-marker-label:before { |
||||
|
border: none; |
||||
|
background: none; |
||||
|
box-shadow: none; |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import { MarkerSettings, FormattedData } from './map-models'; |
||||
|
import { aspectCache, safeExecute, parseTemplate } from '@app/core/utils'; |
||||
|
import { createTooltip } from './maps-utils'; |
||||
|
|
||||
|
export class Marker { |
||||
|
|
||||
|
leafletMarker: L.Marker; |
||||
|
tooltipOffset: [number, number]; |
||||
|
tooltip: L.Popup; |
||||
|
location: L.LatLngExpression; |
||||
|
data: FormattedData; |
||||
|
dataSources: FormattedData[]; |
||||
|
|
||||
|
constructor(private map: L.Map, location: L.LatLngExpression, public settings: MarkerSettings, |
||||
|
data?, dataSources?, onClickListener?, onDragendListener?) { |
||||
|
this.setDataSources(data, dataSources); |
||||
|
this.leafletMarker = L.marker(location, { |
||||
|
draggable: settings.draggableMarker |
||||
|
}); |
||||
|
|
||||
|
this.createMarkerIcon((iconInfo) => { |
||||
|
this.leafletMarker.setIcon(iconInfo.icon); |
||||
|
this.tooltipOffset = [0, -iconInfo.size[1] + 10]; |
||||
|
this.updateMarkerLabel(settings); |
||||
|
this.leafletMarker.addTo(map); |
||||
|
}); |
||||
|
|
||||
|
if (settings.showTooltip) { |
||||
|
this.tooltip = createTooltip(this.leafletMarker, settings); |
||||
|
this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data)); |
||||
|
} |
||||
|
|
||||
|
if (onClickListener) { |
||||
|
this.leafletMarker.on('click', onClickListener); |
||||
|
} |
||||
|
|
||||
|
if (onDragendListener) { |
||||
|
this.leafletMarker.on('dragend', (e) => onDragendListener(e, this.data)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDataSources(data: FormattedData, dataSources: FormattedData[]) { |
||||
|
this.data = data; |
||||
|
this.dataSources = dataSources; |
||||
|
} |
||||
|
|
||||
|
updateMarkerTooltip(data: FormattedData) { |
||||
|
this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data)); |
||||
|
} |
||||
|
|
||||
|
updateMarkerPosition(position: L.LatLngExpression) { |
||||
|
this.leafletMarker.setLatLng(position); |
||||
|
} |
||||
|
|
||||
|
updateMarkerLabel(settings: MarkerSettings) { |
||||
|
this.leafletMarker.unbindTooltip(); |
||||
|
|
||||
|
if (settings.showLabel) { |
||||
|
if (settings.useLabelFunction) { |
||||
|
settings.labelText = safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) |
||||
|
} |
||||
|
else settings.labelText = parseTemplate(settings.label, this.data); |
||||
|
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`, |
||||
|
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateMarkerColor(color) { |
||||
|
this.createDefaultMarkerIcon(color, (iconInfo) => { |
||||
|
this.leafletMarker.setIcon(iconInfo.icon); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateMarkerIcon(settings: MarkerSettings) { |
||||
|
this.createMarkerIcon((iconInfo) => { |
||||
|
this.leafletMarker.setIcon(iconInfo.icon); |
||||
|
this.tooltipOffset = [0, -iconInfo.size[1] + 10]; |
||||
|
this.updateMarkerLabel(settings); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
createMarkerIcon(onMarkerIconReady) { |
||||
|
|
||||
|
if (this.settings.icon) { |
||||
|
onMarkerIconReady({ |
||||
|
size: [30, 30], |
||||
|
icon: this.settings.icon, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const currentImage = this.settings.useMarkerImageFunction ? |
||||
|
safeExecute(this.settings.markerImageFunction, |
||||
|
[this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; |
||||
|
|
||||
|
|
||||
|
if (currentImage && currentImage.url) { |
||||
|
aspectCache(currentImage.url).subscribe( |
||||
|
(aspect) => { |
||||
|
if (aspect) { |
||||
|
let width; |
||||
|
let height; |
||||
|
if (aspect > 1) { |
||||
|
width = currentImage.size; |
||||
|
height = currentImage.size / aspect; |
||||
|
} else { |
||||
|
width = currentImage.size * aspect; |
||||
|
height = currentImage.size; |
||||
|
} |
||||
|
const icon = L.icon({ |
||||
|
iconUrl: currentImage.url, |
||||
|
iconSize: [width, height], |
||||
|
iconAnchor: [width / 2, height], |
||||
|
popupAnchor: [0, -height] |
||||
|
}); |
||||
|
const iconInfo = { |
||||
|
size: [width, height], |
||||
|
icon |
||||
|
}; |
||||
|
onMarkerIconReady(iconInfo); |
||||
|
} else { |
||||
|
this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
createDefaultMarkerIcon(color, onMarkerIconReady) { |
||||
|
const pinColor = color.substr(1); |
||||
|
const icon = L.icon({ |
||||
|
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor, |
||||
|
iconSize: [21, 34], |
||||
|
iconAnchor: [10, 34], |
||||
|
popupAnchor: [0, -34], |
||||
|
shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', |
||||
|
shadowSize: [40, 37], |
||||
|
shadowAnchor: [12, 35] |
||||
|
}); |
||||
|
const iconInfo = { |
||||
|
size: [21, 34], |
||||
|
icon |
||||
|
}; |
||||
|
onMarkerIconReady(iconInfo); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
removeMarker() { |
||||
|
/* this.map$.subscribe(map => |
||||
|
this.leafletMarker.addTo(map))*/ |
||||
|
} |
||||
|
|
||||
|
extendBoundsWithMarker(bounds) { |
||||
|
bounds.extend(this.leafletMarker.getLatLng()); |
||||
|
} |
||||
|
|
||||
|
getMarkerPosition() { |
||||
|
return this.leafletMarker.getLatLng(); |
||||
|
} |
||||
|
|
||||
|
setMarkerPosition(latLng) { |
||||
|
this.leafletMarker.setLatLng(latLng); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L, { LatLngExpression } from 'leaflet'; |
||||
|
import { createTooltip } from './maps-utils'; |
||||
|
import { PolygonSettings } from './map-models'; |
||||
|
|
||||
|
export class Polygon { |
||||
|
|
||||
|
leafletPoly: L.Polygon; |
||||
|
tooltip; |
||||
|
|
||||
|
constructor(public map, coordinates, dataSources, settings: PolygonSettings, onClickListener?) { |
||||
|
this.leafletPoly = L.polygon(coordinates, { |
||||
|
fill: true, |
||||
|
fillColor: settings.polygonColor, |
||||
|
color: settings.polygonStrokeColor, |
||||
|
weight: settings.polygonStrokeWeight, |
||||
|
fillOpacity: settings.polygonOpacity, |
||||
|
opacity: settings.polygonStrokeOpacity |
||||
|
}).addTo(this.map); |
||||
|
|
||||
|
if (settings.showTooltip) { |
||||
|
this.tooltip = createTooltip(this.leafletPoly, settings); |
||||
|
} |
||||
|
if (onClickListener) { |
||||
|
this.leafletPoly.on('click', onClickListener); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
removePolygon() { |
||||
|
this.map.removeLayer(this.leafletPoly); |
||||
|
} |
||||
|
|
||||
|
updatePolygonColor(settings) { |
||||
|
console.log('Polygon -> updatePolygonColor -> settings', settings) |
||||
|
const style: L.PathOptions = { |
||||
|
|
||||
|
fill: true, |
||||
|
fillColor: settings.color, |
||||
|
color: settings.color, |
||||
|
weight: settings.polygonStrokeWeight, |
||||
|
fillOpacity: settings.polygonOpacity, |
||||
|
opacity: settings.polygonStrokeOpacity |
||||
|
}; |
||||
|
this.leafletPoly.setStyle(style); |
||||
|
} |
||||
|
|
||||
|
getPolygonLatLngs() { |
||||
|
return this.leafletPoly.getLatLngs(); |
||||
|
} |
||||
|
|
||||
|
setPolygonLatLngs(latLngs: LatLngExpression[]) { |
||||
|
this.leafletPoly.setLatLngs(latLngs); |
||||
|
this.leafletPoly.redraw(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L, { PolylineDecoratorOptions } from 'leaflet'; |
||||
|
import 'leaflet-polylinedecorator'; |
||||
|
|
||||
|
import { safeExecute } from '@app/core/utils'; |
||||
|
import { PolylineSettings } from './map-models'; |
||||
|
|
||||
|
export class Polyline { |
||||
|
|
||||
|
leafletPoly: L.Polyline; |
||||
|
polylineDecorator: L.PolylineDecorator; |
||||
|
dataSources; |
||||
|
data; |
||||
|
|
||||
|
constructor(private map: L.Map, locations, data, dataSources, settings: PolylineSettings) { |
||||
|
this.dataSources = dataSources; |
||||
|
this.data = data; |
||||
|
|
||||
|
this.leafletPoly = L.polyline(locations, |
||||
|
this.getPolyStyle(settings) |
||||
|
).addTo(this.map); |
||||
|
if (settings.usePolylineDecorator) { |
||||
|
this.polylineDecorator = L.polylineDecorator(this.leafletPoly, this.getDecoratorSettings(settings)).addTo(this.map); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getDecoratorSettings(settings: PolylineSettings): PolylineDecoratorOptions { |
||||
|
return { |
||||
|
patterns: [ |
||||
|
{ |
||||
|
offset: settings.decoratorOffset, |
||||
|
endOffset: settings.endDecoratorOffset, |
||||
|
repeat: settings.decoratorRepeat, |
||||
|
symbol: L.Symbol[settings.decoratorSymbol]({ |
||||
|
pixelSize: settings.decoratorSymbolSize, |
||||
|
polygon: false, |
||||
|
pathOptions: { |
||||
|
color: settings.useDecoratorCustomColor ? settings.decoratorCustomColor : this.getPolyStyle(settings).color, |
||||
|
stroke: true |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
], |
||||
|
interactive: false, |
||||
|
} as PolylineDecoratorOptions |
||||
|
} |
||||
|
|
||||
|
updatePolyline(settings, data, dataSources) { |
||||
|
this.leafletPoly.setStyle(this.getPolyStyle(settings)); |
||||
|
// this.setPolylineLatLngs(data);
|
||||
|
if(this.polylineDecorator) |
||||
|
this.polylineDecorator.setPaths(this.leafletPoly); |
||||
|
} |
||||
|
|
||||
|
getPolyStyle(settings: PolylineSettings): L.PolylineOptions { |
||||
|
return { |
||||
|
color: settings.useColorFunction ? |
||||
|
safeExecute(settings.colorFunction, [this.data, this.dataSources, this.data[0]?.dsIndex]) : settings.color, |
||||
|
opacity: settings.useStrokeOpacityFunction ? |
||||
|
safeExecute(settings.strokeOpacityFunction, [this.data, this.dataSources, this.data[0]?.dsIndex]) : settings.strokeOpacity, |
||||
|
weight: settings.useStrokeWeightFunction ? |
||||
|
safeExecute(settings.strokeWeightFunction, [this.data, this.dataSources, this.data[0]?.dsIndex]) : settings.strokeWeight, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
removePolyline() { |
||||
|
this.map.removeLayer(this.leafletPoly); |
||||
|
} |
||||
|
|
||||
|
getPolylineLatLngs() { |
||||
|
return this.leafletPoly.getLatLngs(); |
||||
|
} |
||||
|
|
||||
|
setPolylineLatLngs(latLngs) { |
||||
|
this.leafletPoly.setLatLngs(latLngs); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import LeafletMap from '../leaflet-map'; |
||||
|
import { MapSettings, UnitedMapSettings } from '../map-models'; |
||||
|
import 'leaflet.gridlayer.googlemutant'; |
||||
|
|
||||
|
let googleLoaded = false; |
||||
|
|
||||
|
|
||||
|
export class GoogleMap extends LeafletMap { |
||||
|
constructor($container, options: UnitedMapSettings) { |
||||
|
|
||||
|
super($container, options); |
||||
|
this.loadGoogle(() => { |
||||
|
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); |
||||
|
const roads = (L.gridLayer as any).googleMutant({ |
||||
|
type: options?.gmDefaultMapType || 'roadmap' |
||||
|
}).addTo(map); |
||||
|
super.setMap(map); |
||||
|
}, options.credentials.apiKey); |
||||
|
super.initSettings(options); |
||||
|
} |
||||
|
|
||||
|
private loadGoogle(callback, apiKey = 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q') { |
||||
|
if (googleLoaded) { |
||||
|
callback() |
||||
|
} |
||||
|
else { |
||||
|
googleLoaded = true; |
||||
|
const script = document.createElement('script'); |
||||
|
script.onload = () => { |
||||
|
callback(); |
||||
|
} |
||||
|
script.onerror = () => { |
||||
|
googleLoaded = false; |
||||
|
} |
||||
|
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`; |
||||
|
document.getElementsByTagName('head')[0].appendChild(script); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import LeafletMap from '../leaflet-map'; |
||||
|
import { MapSettings, UnitedMapSettings } from '../map-models'; |
||||
|
|
||||
|
export class HEREMap extends LeafletMap { |
||||
|
constructor($container, options: UnitedMapSettings) { |
||||
|
const defaultCredentials = |
||||
|
{ |
||||
|
app_id: 'AhM6TzD9ThyK78CT3ptx', |
||||
|
app_code: 'p6NPiITB3Vv0GMUFnkLOOg' |
||||
|
} |
||||
|
super($container, options); |
||||
|
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); |
||||
|
const tileLayer = (L.tileLayer as any).provider(options.mapProvider || 'HERE.normalDay', options.credentials || defaultCredentials); |
||||
|
tileLayer.addTo(map); |
||||
|
super.setMap(map); |
||||
|
super.initSettings(options); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import LeafletMap from '../leaflet-map'; |
||||
|
import { MapSettings, UnitedMapSettings } from '../map-models'; |
||||
|
import { aspectCache } from '@app/core/utils'; |
||||
|
|
||||
|
const maxZoom = 4;// ?
|
||||
|
|
||||
|
export class ImageMap extends LeafletMap { |
||||
|
|
||||
|
imageOverlay; |
||||
|
aspect = 0; |
||||
|
width = 0; |
||||
|
height = 0; |
||||
|
|
||||
|
constructor($container: HTMLElement, options: UnitedMapSettings) { |
||||
|
super($container, options); |
||||
|
aspectCache(options.mapUrl).subscribe(aspect => { |
||||
|
this.aspect = aspect; |
||||
|
this.onResize(); |
||||
|
super.setMap(this.map); |
||||
|
super.initSettings(options); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateBounds(updateImage?, lastCenterPos?) { |
||||
|
const w = this.width; |
||||
|
const h = this.height; |
||||
|
let southWest = this.pointToLatLng(0, h); |
||||
|
let northEast = this.pointToLatLng(w, 0); |
||||
|
const bounds = new L.LatLngBounds(southWest, northEast); |
||||
|
|
||||
|
if (updateImage && this.imageOverlay) { |
||||
|
this.imageOverlay.remove(); |
||||
|
this.imageOverlay = null; |
||||
|
} |
||||
|
|
||||
|
if (this.imageOverlay) { |
||||
|
this.imageOverlay.setBounds(bounds); |
||||
|
} else { |
||||
|
this.imageOverlay = L.imageOverlay(this.options.mapUrl, bounds).addTo(this.map); |
||||
|
|
||||
|
} |
||||
|
const padding = 200 * maxZoom; |
||||
|
southWest = this.pointToLatLng(-padding, h + padding); |
||||
|
northEast = this.pointToLatLng(w + padding, -padding); |
||||
|
const maxBounds = new L.LatLngBounds(southWest, northEast); |
||||
|
this.map.setMaxBounds(maxBounds); |
||||
|
if (lastCenterPos) { |
||||
|
lastCenterPos.x *= w; |
||||
|
lastCenterPos.y *= h; |
||||
|
/* this.ctx.$scope.$injector.get('$mdUtil').nextTick(() => { |
||||
|
this.map.panTo(center, { animate: false }); |
||||
|
});*/ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onResize(updateImage?) { |
||||
|
let width = this.$container.clientWidth; |
||||
|
if (width > 0 && this.aspect) { |
||||
|
let height = width / this.aspect; |
||||
|
const imageMapHeight = this.$container.clientHeight; |
||||
|
if (imageMapHeight > 0 && height > imageMapHeight) { |
||||
|
height = imageMapHeight; |
||||
|
width = height * this.aspect; |
||||
|
} |
||||
|
width *= maxZoom; |
||||
|
const prevWidth = this.width; |
||||
|
const prevHeight = this.height; |
||||
|
if (this.width !== width) { |
||||
|
this.width = width; |
||||
|
this.height = width / this.aspect; |
||||
|
if (!this.map) { |
||||
|
this.initMap(updateImage); |
||||
|
} else { |
||||
|
const lastCenterPos = this.latLngToPoint(this.map.getCenter()); |
||||
|
lastCenterPos.x /= prevWidth; |
||||
|
lastCenterPos.y /= prevHeight; |
||||
|
this.updateBounds(updateImage, lastCenterPos); |
||||
|
this.map.invalidateSize(true); |
||||
|
// this.updateMarkers();
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
initMap(updateImage?) { |
||||
|
if (!this.map && this.aspect > 0) { |
||||
|
const center = this.pointToLatLng(this.width / 2, this.height / 2); |
||||
|
this.map = L.map(this.$container, { |
||||
|
minZoom: 1, |
||||
|
maxZoom, |
||||
|
scrollWheelZoom: !this.options.disableScrollZooming, |
||||
|
center, |
||||
|
zoom: 1, |
||||
|
crs: L.CRS.Simple, |
||||
|
attributionControl: false |
||||
|
}); |
||||
|
this.updateBounds(updateImage); |
||||
|
// this.updateMarkers();
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
convertPosition(expression): L.LatLng { |
||||
|
return this.pointToLatLng( |
||||
|
expression[this.options.xPosKeyName] * this.width, |
||||
|
expression[this.options.yPosKeyName] * this.height); |
||||
|
} |
||||
|
|
||||
|
pointToLatLng(x, y): L.LatLng { |
||||
|
return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, maxZoom - 1); |
||||
|
} |
||||
|
|
||||
|
latLngToPoint(latLng) { |
||||
|
return L.CRS.Simple.latLngToPoint(latLng, maxZoom - 1); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
export * from './tencent-map'; |
||||
|
export * from './google-map'; |
||||
|
export * from './here-map'; |
||||
|
export * from './image-map'; |
||||
|
export * from './openstreet-map'; |
||||
@ -0,0 +1,30 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import LeafletMap from '../leaflet-map'; |
||||
|
import { MapSettings, UnitedMapSettings } from '../map-models'; |
||||
|
|
||||
|
export class OpenStreetMap extends LeafletMap { |
||||
|
constructor($container, options: UnitedMapSettings) { |
||||
|
super($container, options); |
||||
|
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); |
||||
|
const tileLayer = (L.tileLayer as any).provider('OpenStreetMap.Mapnik'); |
||||
|
tileLayer.addTo(map); |
||||
|
super.setMap(map); |
||||
|
super.initSettings(options); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import LeafletMap from '../leaflet-map'; |
||||
|
import { MapSettings, UnitedMapSettings } from '../map-models'; |
||||
|
|
||||
|
export class TencentMap extends LeafletMap { |
||||
|
constructor($container, options: UnitedMapSettings) { |
||||
|
super($container, options); |
||||
|
const txUrl = 'http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={y}&type=vector&style=0'; |
||||
|
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); |
||||
|
const txLayer = L.tileLayer(txUrl, { subdomains: '0123', tms: true }).addTo(map); |
||||
|
txLayer.addTo(map); |
||||
|
super.setMap(map); |
||||
|
super.initSettings(options); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,37 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="trip-animation-widget"> |
||||
|
<div class="trip-animation-label-container" *ngIf="settings.showLabel"> |
||||
|
{{settings.label | tbParseTemplate: activeTrip}} |
||||
|
</div> |
||||
|
<div class="trip-animation-container" layout="column"> |
||||
|
<div class="map" #map></div> |
||||
|
<div class="trip-animation-info-panel" layout="row"> |
||||
|
<button class="tooltip-button" mat-mini-fab color="primary" aria-label="tooltip" |
||||
|
*ngIf="settings.showTooltip" (click)="showHideTooltip()"> |
||||
|
<mat-icon>info_outline</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" |
||||
|
[ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}" [innerHTML]="mainTooltip" |
||||
|
[ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-history-selector *ngIf="historicalData" [settings]="settings" [intervals]="intervals" |
||||
|
(onTimeUpdated)="timeUpdated($event)"></tb-history-selector> |
||||
|
</div> |
||||
@ -0,0 +1,89 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
.trip-animation-widget { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
font-size: 16px; |
||||
|
line-height: 24px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
|
||||
|
.trip-animation-label-container { |
||||
|
height: 24px; |
||||
|
} |
||||
|
|
||||
|
.trip-animation-container { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
flex: 1; |
||||
|
width: 100%; |
||||
|
|
||||
|
.map { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.trip-animation-info-panel { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
pointer-events: none; |
||||
|
|
||||
|
.tooltip-button { |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 32px; |
||||
|
min-width: 32px; |
||||
|
height: 32px; |
||||
|
min-height: 32px; |
||||
|
padding: 0 0 2px; |
||||
|
margin: 2px; |
||||
|
line-height: 24px; |
||||
|
z-index: 999; |
||||
|
|
||||
|
&::ng-deep .mat-button-wrapper { |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
mat-icon { |
||||
|
width: 24px; |
||||
|
height: 24px; |
||||
|
|
||||
|
svg { |
||||
|
width: inherit; |
||||
|
height: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.trip-animation-tooltip { |
||||
|
position: absolute; |
||||
|
top: 38px; |
||||
|
right: 0; |
||||
|
z-index: 400; |
||||
|
padding: 10px; |
||||
|
background-color: #fff; |
||||
|
transition: 0.3s ease-in-out; |
||||
|
|
||||
|
&-hidden { |
||||
|
transform: translateX(110%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import L from 'leaflet'; |
||||
|
import _ from 'lodash'; |
||||
|
import tinycolor from 'tinycolor2'; |
||||
|
import { interpolateOnPointSegment } from 'leaflet-geometryutil'; |
||||
|
|
||||
|
import { Component, OnInit, Input, ViewChild, AfterViewInit, ChangeDetectorRef, SecurityContext } from '@angular/core'; |
||||
|
import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; |
||||
|
import { MapProviders } from '../lib/maps/map-models'; |
||||
|
import { parseArray, parseTemplate, safeExecute } from '@app/core/utils'; |
||||
|
import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils'; |
||||
|
import { tripAnimationSchema } from '../lib/maps/schemes'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { WidgetConfig, JsonSchema, JsonSettingsSchema } from '@app/shared/public-api'; |
||||
|
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; |
||||
|
import { getRatio, findAngle } from '../lib/maps/maps-utils'; |
||||
|
|
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'trip-animation', |
||||
|
templateUrl: './trip-animation.component.html', |
||||
|
styleUrls: ['./trip-animation.component.scss'] |
||||
|
}) |
||||
|
export class TripAnimationComponent implements OnInit, AfterViewInit { |
||||
|
|
||||
|
constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { } |
||||
|
|
||||
|
@Input() ctx: WidgetContext; |
||||
|
|
||||
|
@ViewChild('map') mapContainer; |
||||
|
|
||||
|
mapWidget: MapWidgetController; |
||||
|
historicalData; |
||||
|
intervals; |
||||
|
normalizationStep = 1000; |
||||
|
interpolatedData = []; |
||||
|
widgetConfig: WidgetConfig; |
||||
|
settings; |
||||
|
mainTooltip = ''; |
||||
|
visibleTooltip = false; |
||||
|
activeTrip; |
||||
|
|
||||
|
static getSettingsSchema(): JsonSettingsSchema { |
||||
|
const schema = initSchema(); |
||||
|
addToSchema(schema, TbMapWidgetV2.getProvidersSchema()); |
||||
|
addGroupInfo(schema, 'Map Provider Settings'); |
||||
|
addToSchema(schema, tripAnimationSchema); |
||||
|
addGroupInfo(schema, 'Trip Animation Settings'); |
||||
|
return schema; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.widgetConfig = this.ctx.widgetConfig; |
||||
|
const settings = { |
||||
|
normalizationStep: 1000, |
||||
|
showLabel: false, |
||||
|
buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), |
||||
|
disabledButtonColor: tinycolor(this.widgetConfig.color).setAlpha(0.3).toRgbString(), |
||||
|
rotationAngle: 0 |
||||
|
} |
||||
|
this.settings = { ...settings, ...this.ctx.settings }; |
||||
|
const subscription = this.ctx.subscriptions[Object.keys(this.ctx.subscriptions)[0]]; |
||||
|
if (subscription) subscription.callbacks.onDataUpdated = (updated) => { |
||||
|
this.historicalData = parseArray(this.ctx.data); |
||||
|
this.activeTrip = this.historicalData[0][0]; |
||||
|
this.calculateIntervals(); |
||||
|
this.timeUpdated(this.intervals[0]); |
||||
|
this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds))); |
||||
|
|
||||
|
this.mapWidget.map.map?.invalidateSize(); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
const ctxCopy: WidgetContext = _.cloneDeep(this.ctx); |
||||
|
ctxCopy.settings.showLabel = false; |
||||
|
ctxCopy.settings.showTooltip = false; |
||||
|
this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, ctxCopy, this.mapContainer.nativeElement); |
||||
|
} |
||||
|
|
||||
|
timeUpdated(time: number) { |
||||
|
const currentPosition = this.interpolatedData.map(dataSource => dataSource[time]); |
||||
|
this.activeTrip = currentPosition[0]; |
||||
|
if (this.settings.showPolygon) { |
||||
|
this.mapWidget.map.updatePolygons(this.interpolatedData); |
||||
|
} |
||||
|
this.mapWidget.map.updateMarkers(currentPosition); |
||||
|
} |
||||
|
|
||||
|
setActiveTrip() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
calculateIntervals() { |
||||
|
this.historicalData.forEach((dataSource, index) => { |
||||
|
this.intervals = []; |
||||
|
for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) { |
||||
|
this.intervals.push(time); |
||||
|
} |
||||
|
this.intervals.push(dataSource[dataSource.length - 1]?.time); |
||||
|
this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
showHideTooltip() { |
||||
|
const tooltipText: string = this.settings.useTooltipFunction ? |
||||
|
safeExecute(this.settings.tooolTipFunction, [this.activeTrip, this.historicalData, 0]) |
||||
|
: this.settings.tooltipPattern; |
||||
|
|
||||
|
this.mainTooltip = this.sanitizer.sanitize(SecurityContext.HTML, parseTemplate(tooltipText, this.activeTrip)) |
||||
|
this.visibleTooltip = !this.visibleTooltip; |
||||
|
} |
||||
|
|
||||
|
interpolateArray(originData, interpolatedIntervals) { |
||||
|
|
||||
|
const result = {}; |
||||
|
|
||||
|
for (let i = 1, j = 0; i < originData.length && j < interpolatedIntervals.length;) { |
||||
|
const currentTime = interpolatedIntervals[j]; |
||||
|
while (originData[i].time < currentTime) i++; |
||||
|
const before = originData[i - 1]; |
||||
|
const after = originData[i]; |
||||
|
const interpolation = interpolateOnPointSegment( |
||||
|
new L.Point(before.latitude, before.longitude), |
||||
|
new L.Point(after.latitude, after.longitude), |
||||
|
getRatio(before.time, after.time, currentTime)); |
||||
|
result[currentTime] = ({ |
||||
|
...originData[i], |
||||
|
rotationAngle: findAngle(before, after) + this.settings.rotationAngle, |
||||
|
latitude: interpolation.x, |
||||
|
longitude: interpolation.y |
||||
|
}); |
||||
|
j++; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export let TbTripAnimationWidget = TripAnimationComponent; |
||||
|
|
||||
@ -0,0 +1,51 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="trip-animation-control-panel"> |
||||
|
<div> |
||||
|
<button mat-icon-button class="mat-icon-button" aria-label="Start" (click)="moveStart()"> |
||||
|
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">fast_rewind</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button class="mat-icon-button" aria-label="Previous" (click)="movePrev()"> |
||||
|
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_previous</mat-icon> |
||||
|
</button> |
||||
|
<mat-slider [(ngModel)]="index" [min]="minTimeIndex" [max]="maxTimeIndex" (change)="changeIndex()"> |
||||
|
</mat-slider> |
||||
|
<button mat-icon-button class="mat-icon-button" aria-label="Next" (click)="moveNext()"> |
||||
|
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_next</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button class="mat-icon-button" aria-label="End" (click)="moveEnd()"> |
||||
|
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">fast_forward</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button class="mat-icon-button" aria-label="Play"> |
||||
|
<mat-icon (click)="play()" *ngIf="!playing" class="material-icons" |
||||
|
[ngStyle]="{'color': settings.buttonColor}"> |
||||
|
play_circle_outline |
||||
|
</mat-icon> |
||||
|
<mat-icon (click)="pause()" *ngIf="playing" class="material-icons" |
||||
|
[ngStyle]="{'color': settings.buttonColor}"> |
||||
|
pause_circle_outline |
||||
|
</mat-icon> |
||||
|
</button> |
||||
|
<mat-select matInput [(ngModel)]="speed" (selectionChange)="reeneble()" class="speed-select" aria-label="Speed selector"> |
||||
|
<mat-option [value]="speedValue" flex="1" *ngFor="let speedValue of speeds">{{speedValue}} </mat-option> |
||||
|
</mat-select> |
||||
|
</div> |
||||
|
<div class="panel-timer"> |
||||
|
<span *ngIf="this.intervals[this.index]">{{ this.intervals[this.index] | date:'medium'}}</span> |
||||
|
<span *ngIf="!this.intervals[this.index]">{{ "widget.no-data-found" | translate}}</span> |
||||
|
</div> |
||||
@ -0,0 +1,140 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
.trip-animation-widget { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
font-size: 16px; |
||||
|
line-height: 24px; |
||||
|
|
||||
|
.trip-animation-label-container { |
||||
|
height: 24px; |
||||
|
} |
||||
|
|
||||
|
.trip-animation-container { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
flex: 1; |
||||
|
width: 100%; |
||||
|
|
||||
|
#trip-animation-map { |
||||
|
z-index: 1; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
|
||||
|
.pointsLayerMarkerIcon { |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.trip-animation-info-panel { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
z-index: 2; |
||||
|
pointer-events: none; |
||||
|
|
||||
|
.md-button { |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 32px; |
||||
|
min-width: 32px; |
||||
|
height: 32px; |
||||
|
min-height: 32px; |
||||
|
padding: 0 0 2px; |
||||
|
margin: 2px; |
||||
|
line-height: 24px; |
||||
|
|
||||
|
ng-md-icon { |
||||
|
width: 24px; |
||||
|
height: 24px; |
||||
|
|
||||
|
svg { |
||||
|
width: inherit; |
||||
|
height: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.trip-animation-tooltip { |
||||
|
position: absolute; |
||||
|
top: 38px; |
||||
|
right: 0; |
||||
|
z-index: 2; |
||||
|
padding: 10px; |
||||
|
background-color: #fff; |
||||
|
transition: 0.3s ease-in-out; |
||||
|
|
||||
|
&-hidden { |
||||
|
transform: translateX(110%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.trip-animation-control-panel { |
||||
|
position: relative; |
||||
|
box-sizing: border-box; |
||||
|
width: 100%; |
||||
|
padding-bottom: 16px; |
||||
|
padding-left: 10px; |
||||
|
|
||||
|
md-slider-container { |
||||
|
md-slider { |
||||
|
min-width: 80px; |
||||
|
} |
||||
|
|
||||
|
button.md-button.md-icon-button { |
||||
|
width: 44px; |
||||
|
min-width: 44px; |
||||
|
height: 44px; |
||||
|
min-height: 44px; |
||||
|
margin: 0; |
||||
|
line-height: 28px; |
||||
|
|
||||
|
md-icon { |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
font-size: 28px; |
||||
|
|
||||
|
svg { |
||||
|
width: inherit; |
||||
|
height: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
md-select { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.panel-timer { |
||||
|
max-width: none; |
||||
|
padding-right: 250px; |
||||
|
padding-left: 90px; |
||||
|
margin-top: -20px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.speed-select{ |
||||
|
width: 50px; |
||||
|
margin-left: 20px; |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit, OnChanges, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; |
||||
|
import { interval, Subscription, Subscriber, SubscriptionLike, Observer } from 'rxjs'; |
||||
|
import { filter, tap } from 'rxjs/operators'; |
||||
|
import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-history-selector', |
||||
|
templateUrl: './history-selector.component.html', |
||||
|
styleUrls: ['./history-selector.component.scss'] |
||||
|
}) |
||||
|
export class HistorySelectorComponent implements OnInit, OnChanges { |
||||
|
|
||||
|
@Input() settings: HistorySelectSettings |
||||
|
@Input() intervals = []; |
||||
|
|
||||
|
@Output() timeUpdated: EventEmitter<number> = new EventEmitter(); |
||||
|
|
||||
|
animationTime; |
||||
|
minTimeIndex = 0; |
||||
|
maxTimeIndex = 0; |
||||
|
speed = 1; |
||||
|
index = 0; |
||||
|
playing = false; |
||||
|
interval; |
||||
|
speeds = [1, 5, 10, 25]; |
||||
|
|
||||
|
|
||||
|
constructor(private cd: ChangeDetectorRef) { } |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
ngOnChanges() { |
||||
|
this.maxTimeIndex = this.intervals?.length - 1; |
||||
|
} |
||||
|
|
||||
|
play() { |
||||
|
this.playing = true; |
||||
|
if (!this.interval) |
||||
|
this.interval = interval(1000 / this.speed) |
||||
|
.pipe( |
||||
|
filter(() => this.playing), |
||||
|
tap(() => this.index++)).subscribe(() => { |
||||
|
if (this.index < this.maxTimeIndex) { |
||||
|
this.cd.detectChanges(); |
||||
|
this.timeUpdated.emit(this.intervals[this.index]); |
||||
|
} |
||||
|
else { |
||||
|
this.interval.complete(); |
||||
|
} |
||||
|
}, err => { |
||||
|
console.log(err); |
||||
|
}, () => { |
||||
|
this.index = this.minTimeIndex; |
||||
|
this.playing = false; |
||||
|
this.interval = null; |
||||
|
this.cd.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
reeneble() { |
||||
|
if (this.playing) { |
||||
|
const position = this.index; |
||||
|
this.interval.complete(); |
||||
|
this.index = position; |
||||
|
this.play(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
pause() { |
||||
|
this.playing = false; |
||||
|
this.cd.detectChanges(); |
||||
|
this.timeUpdated.emit(this.intervals[this.index]); |
||||
|
} |
||||
|
|
||||
|
moveNext() { |
||||
|
if (this.index < this.maxTimeIndex) { |
||||
|
this.index++; |
||||
|
} |
||||
|
this.pause(); |
||||
|
} |
||||
|
|
||||
|
movePrev() { |
||||
|
if (this.index > this.minTimeIndex) { |
||||
|
this.index++; |
||||
|
} |
||||
|
this.pause(); |
||||
|
} |
||||
|
|
||||
|
moveStart() { |
||||
|
this.index = this.minTimeIndex; |
||||
|
this.pause(); |
||||
|
} |
||||
|
|
||||
|
moveEnd() { |
||||
|
this.index = this.maxTimeIndex; |
||||
|
this.pause(); |
||||
|
} |
||||
|
|
||||
|
changeIndex() { |
||||
|
this.timeUpdated.emit(this.intervals[this.index]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Pipe, PipeTransform } from '@angular/core'; |
||||
|
import { parseTemplate } from '@app/core/utils'; |
||||
|
|
||||
|
@Pipe({ name: 'tbParseTemplate' }) |
||||
|
export class TbTemplatePipe implements PipeTransform { |
||||
|
transform(template, data): string { |
||||
|
console.log('TbTemplatePipe -> transform -> template, data', template, data) |
||||
|
return parseTemplate(template, data); |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 219 B |
@ -0,0 +1,12 @@ |
|||||
|
import * as L from 'leaflet' |
||||
|
|
||||
|
declare module 'leaflet' { |
||||
|
|
||||
|
namespace Control { |
||||
|
class AddMarker extends L.Control { } |
||||
|
} |
||||
|
|
||||
|
namespace control { |
||||
|
function addMarker(options): Control.AddMarker; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue