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