625 lines
19 KiB
JavaScript
625 lines
19 KiB
JavaScript
/********************************************************************
|
|
* 2D Sensor List for Jetvision MLAT Server
|
|
********************************************************************
|
|
* Guenter Koellner Embedded Development GmbH
|
|
*
|
|
* Am Rain 24
|
|
* 85256 Vierkirchen
|
|
* Geschaeftsfuehrer Guenter Koellner
|
|
*
|
|
* http://www.modesbeast.com
|
|
********************************************************************/
|
|
|
|
var Gmap=null;
|
|
var Planes={};
|
|
var Filters={altitude: [undefined, undefined],
|
|
enable_alt: undefined,
|
|
speed: [undefined, undefined],
|
|
enable_spd: undefined,
|
|
distance: [undefined, undefined],
|
|
enable_dis: undefined,
|
|
typ_enable: undefined,
|
|
typ_filter: undefined,
|
|
fli_enable: undefined,
|
|
fli_filter: undefined,
|
|
tracklength: undefined,
|
|
fleetwatch: undefined,
|
|
reg_enable: undefined,
|
|
refreshtime: undefined
|
|
};
|
|
|
|
var GmapInit={
|
|
Zoomlevel : 6,
|
|
CenterLat : 48.353013,
|
|
CenterLon : 11.785958,
|
|
LatLonSrc : "GPS",
|
|
DistanceUnit : "km",
|
|
ShowRangeRings : false
|
|
};
|
|
|
|
var NumPlanes = 0;
|
|
var Selected=null;
|
|
var RangeCircle={};
|
|
var CircleStep=50;
|
|
var NauticalMiles_to_Meters=1852;
|
|
var KiloMeters_to_Meters=1000;
|
|
|
|
//var map_container;
|
|
var map_box;
|
|
var map_info_box;
|
|
var flight_info_box;
|
|
var hide_info;
|
|
var hide_info_btn;
|
|
|
|
function showInfoBox()
|
|
{
|
|
hide_info.style.visibility='visible';
|
|
map_info_box.style.width='210px';
|
|
hide_info_btn.setAttribute("class", "glyphicon glyphicon-minus");
|
|
}
|
|
|
|
function hideInfoBox()
|
|
{
|
|
hide_info.style.visibility='hidden';
|
|
map_info_box.style.width='1px';
|
|
hide_info_btn.setAttribute("class", "glyphicon glyphicon-plus");
|
|
}
|
|
|
|
function toggleInfoBox()
|
|
{
|
|
if(hide_info.style.visibility == 'hidden')
|
|
showInfoBox();
|
|
else
|
|
hideInfoBox();
|
|
}
|
|
|
|
function get_image_object(ac_category, ac_type)
|
|
{
|
|
return {x:12, y:12, scale:1, rotcorr: 180, path:"M0,0 6,12 12,0z"};
|
|
}
|
|
|
|
|
|
function getIconForPlane(plane)
|
|
{
|
|
var rFill = 255; gFill = 255; bFill = 0;
|
|
var rBorder = 64, gBorder = 64, bBorder = 64;
|
|
|
|
if (plane.timeout == 1) /* after timeout 1 */
|
|
{
|
|
rFill = 128; gFill = 128; bFill = 128; /* dark gray */
|
|
rBorder = rFill; gBorder=gFill; bBorder=bFill;
|
|
}
|
|
|
|
var maxalt = 40000; /* Max altitude in the average case */
|
|
var invalt = maxalt-plane.alt;
|
|
var selected = (Selected == plane.id);
|
|
var imageObj = get_image_object("dummy1", "dummy2");
|
|
|
|
var object_box = Raphael.pathBBox(imageObj.path);
|
|
var offset_x = (object_box.x + object_box.x2) / 2;
|
|
var offset_y = (object_box.y + object_box.y2) / 2;
|
|
|
|
if (invalt < 0) invalt = 0;
|
|
|
|
var strokew = 1;
|
|
if (selected) { strokew = 2; }
|
|
if (plane.timeout == 2) { strokew = 0.333; } /* light on timeout 2 */
|
|
|
|
var ac_symbol = {
|
|
strokeColor: 'rgb('+rBorder+','+gBorder+','+bBorder+')',
|
|
strokeWeight: strokew,
|
|
path: imageObj.path,
|
|
scale: imageObj.scale,
|
|
fillColor: 'rgb('+rFill+','+gFill+','+bFill+')',
|
|
fillOpacity: 0.9,
|
|
anchor: new google.maps.Point(offset_x, offset_y),
|
|
rotation: (plane.trk + imageObj.rotcorr) % 360
|
|
};
|
|
|
|
var myZoom = Gmap.getZoom();
|
|
if((myZoom <= 14) && ((6 < myZoom))) {
|
|
ac_symbol.scale = ac_symbol.scale + 0.2 * ((myZoom-10)/4);
|
|
}
|
|
else if(myZoom < 14) {
|
|
ac_symbol.scale = ac_symbol.scale - 0.2
|
|
}
|
|
else {
|
|
ac_symbol.scale = ac_symbol.scale + 0.2
|
|
}
|
|
|
|
return ac_symbol;
|
|
}
|
|
|
|
|
|
function selectPlane()
|
|
{
|
|
if (!Planes[this.planehex])
|
|
return;
|
|
|
|
var Old_Selected = Selected;
|
|
Selected = this.planehex;
|
|
|
|
if (Planes[Old_Selected])
|
|
{
|
|
/* Remove the highlight in the previously selected plane. */
|
|
Planes[Old_Selected].marker1.setIcon(getIconForPlane(Planes[Old_Selected]));
|
|
labelSetStyle(Planes[Old_Selected], Planes[Old_Selected].marker1);
|
|
}
|
|
|
|
Planes[Selected].marker1.setIcon(getIconForPlane(Planes[Selected]));
|
|
labelSetStyle(Planes[Selected],Planes[Selected].marker1);
|
|
|
|
refreshSelectedInfo();
|
|
showInfoBox();
|
|
}
|
|
|
|
|
|
function deselectPlane()
|
|
{
|
|
if (Selected) {
|
|
var Old_Selected = Selected;
|
|
Selected = null;
|
|
Planes[Old_Selected].marker1.setIcon(getIconForPlane(Planes[Old_Selected]));
|
|
labelSetStyle(Planes[Old_Selected],Planes[Old_Selected].marker1);
|
|
flight_info_box.innerHTML = "";
|
|
}
|
|
}
|
|
|
|
|
|
function sortPlaneSymbolList(a, b)
|
|
{
|
|
var al = a.id;
|
|
var bl = b.id;
|
|
|
|
if (al < bl) return 1;
|
|
if (al > bl) return -1;
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* Check if an aircraft disappeared and therefore was deselected */
|
|
function checkAircraftDeselected()
|
|
{
|
|
if (Selected == null)
|
|
{
|
|
}
|
|
}
|
|
|
|
function getTimeString(isLocal) {
|
|
var hours;
|
|
var minutes;
|
|
var date = new Date();
|
|
var zone_offset = date.getTimezoneOffset();
|
|
|
|
if(isLocal)
|
|
{
|
|
hours = date.getHours();
|
|
minutes = date.getMinutes();
|
|
}
|
|
else
|
|
{
|
|
hours = date.getUTCHours();
|
|
minutes = date.getUTCMinutes();
|
|
}
|
|
|
|
if(hours < 10) {hours = '0'+hours;}
|
|
if(minutes < 10) {minutes = '0'+minutes;}
|
|
|
|
var zeit_string = hours + ':' + minutes;
|
|
|
|
return zeit_string;
|
|
}
|
|
|
|
/* Info box in div map_info_box - 0=Aircraft Info 1=Config */
|
|
function refreshGeneralInfo()
|
|
{
|
|
var html = '';
|
|
|
|
html += "<table class=\"info_box_table\">";
|
|
html += " <thead>";
|
|
|
|
html += " <tr class=\"info_header\">";
|
|
html += " <td colspan=\"2\"><span class=\"glyphicon glyphicon-time\" aria-hidden=\"true\"></span> General Information</td>";
|
|
html += " </tr>";
|
|
|
|
html += " </thead>";
|
|
html += " <tbody>";
|
|
|
|
html += " <tr class=\"info_label\">";
|
|
html += " <td>UTC Time</td>";
|
|
html += " <td>Local Time</td>";
|
|
html += " </tr>";
|
|
|
|
html += " <tr class=\"info_value\">";
|
|
html += " <td>" + getTimeString(false) + "</td>";
|
|
html += " <td>" + getTimeString(true) + "</td>";
|
|
html += " </tr>";
|
|
|
|
html += " <tr class=\"info_label\">";
|
|
html += " <td colspan=\"2\">Number of Aircraft on Map</td>";
|
|
html += " </tr>";
|
|
|
|
html += " <tr class=\"info_value\">";
|
|
html += " <td colspan=\"2\">" + NumPlanes +"</td>";
|
|
html += " </tr>";
|
|
|
|
html += " </tbody>";
|
|
html += "</table>";
|
|
|
|
document.getElementById('general_info_box').innerHTML = html;
|
|
}
|
|
|
|
|
|
function refreshSelectedInfo()
|
|
{
|
|
var p = undefined;
|
|
|
|
if(Selected)
|
|
p = Planes[Selected];
|
|
|
|
if (p && Selected)
|
|
{
|
|
var html = '';
|
|
|
|
html += '<table class="info_box_table">';
|
|
|
|
/* AC Details Header */
|
|
html += ' <tr class="info_header">';
|
|
html += ' <td colspan="2"><span class="glyphicon glyphicon-plane" aria-hidden="true"></span> Aircraft Details</td>';
|
|
html += ' </tr>';
|
|
|
|
/* AC Details Row 1 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td width="50%">Registration</td>';
|
|
html += ' <td width="50%">Mode-S</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
if(p.reg != "") {
|
|
html += ' <td><a href="https://planespotters.net/search?q='
|
|
+ encodeURIComponent(p.reg) + '" target="_blank">' + p.reg + '</a> </td>';
|
|
} else {
|
|
html += ' <td>' + p.reg + ' </td>';
|
|
}
|
|
html += ' <td>'+ p.id +' </td>';
|
|
html += ' </tr>';
|
|
|
|
/* AC Details Row 2 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Type</td>';
|
|
html += ' <td>Category</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.typ + ' </td>';
|
|
html += ' <td>' + p.cat + ' </td>';
|
|
html += ' </tr>';
|
|
|
|
/* Flight Details Header */
|
|
html += ' <tr class="info_header">';
|
|
if (p.fli.length) {
|
|
html += ' <td colspan="2"><span class="glyphicon glyphicon-plane" aria-hidden="true"></span> Flight '+ p.fli +'</td>';
|
|
} else {
|
|
html += ' <td colspan="2"><span class="glyphicon glyphicon-plane" aria-hidden="true"></span> Flight Details</td>';
|
|
}
|
|
html += ' </tr>';
|
|
|
|
/* Flight Details Row 1 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Origin</td>';
|
|
html += ' <td>Destination</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.org + ' </td>';
|
|
html += ' <td>' + p.dst + ' </td>';
|
|
html += ' </tr>';
|
|
|
|
/* Flight Details Row 2 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Track</td>';
|
|
html += ' <td>Speed</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.trk + '°</td>';
|
|
html += ' <td>' + p.spd + ' kt</td>';
|
|
html += ' </tr>';
|
|
|
|
/* Flight Details Row 3 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Altitude</td>';
|
|
html += ' <td>Vertical Rate</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.alt + ' ft</td>';
|
|
if(p.vrt) {
|
|
html += ' <td>' + p.vrt + ' ft/min</td>';
|
|
} else {
|
|
html += ' <td></td>';
|
|
}
|
|
html += ' </tr>';
|
|
|
|
/* AC Tracking Header */
|
|
html += ' <tr class="info_header">';
|
|
html += ' <td colspan="2"><span class="glyphicon glyphicon-map-marker" aria-hidden="true"></span> Aircraft Tracking</td>';
|
|
html += ' </tr>';
|
|
|
|
/* AC Tracking Row 1 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Latitude</td>';
|
|
html += ' <td>Longitude</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.lat + '°</td>';
|
|
html += ' <td>' + p.lon + '°</td>';
|
|
html += ' </tr>';
|
|
|
|
/* AC Tracking Row 2 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Distance</td>';
|
|
html += ' <td>Source</td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + "@PH1" + ' ' + DistanceUnit + '</td>';
|
|
html += ' <td>@PH2</td>';
|
|
html += ' </tr>';
|
|
|
|
/* AC Tracking Row 3 */
|
|
html += ' <tr class="info_label">';
|
|
html += ' <td>Last Signal</td>';
|
|
html += ' <td> </td>';
|
|
html += ' </tr>';
|
|
html += ' <tr class="info_value">';
|
|
html += ' <td>' + p.lla + ' sec</td>';
|
|
html += ' <td> </td>';
|
|
html += ' </tr>';
|
|
|
|
html += '</table>';
|
|
|
|
flight_info_box.innerHTML = html;
|
|
}
|
|
else
|
|
{
|
|
/* Remove Selected Info */
|
|
flight_info_box.innerHTML = "<p>Click on a client for more info.</p>";
|
|
}
|
|
}
|
|
|
|
// This is the text that appears with all displayed aircraft
|
|
function labelDataTable(plane)
|
|
{
|
|
var labelDataTab = "<table class=\"aircraft_label\">" +
|
|
"<tr><td class=\"flight\">" + plane.id + "</td><td class=\"altitude\">" + plane.ac + "</td></tr>" +
|
|
"<tr><td class=\"verticalRate\">" + plane.iBy + "</td><td class=\"speed\">" + plane.iMsg + "</td></tr>" +
|
|
"</table>";
|
|
return labelDataTab;
|
|
}
|
|
|
|
|
|
function labelSetAnchor(plane, marker, offset)
|
|
{
|
|
if (plane.trk > 315) { marker.set("labelAnchor", new google.maps.Point( 66, -1 + offset)); return; }
|
|
if (plane.trk < 45) { marker.set("labelAnchor", new google.maps.Point( 66, -1 + offset)); return; }
|
|
if (plane.trk < 135) { marker.set("labelAnchor", new google.maps.Point( 37, -1 + 28 + offset)); return; }
|
|
if (plane.trk < 225) { marker.set("labelAnchor", new google.maps.Point(-17, -1 + offset)); return; }
|
|
marker.set("labelAnchor", new google.maps.Point(37, -1 - 28 + offset));
|
|
}
|
|
|
|
// https://www.quackit.com/html/codes/color/html_color_chart.cfm
|
|
function labelSetStyle(plane, marker)
|
|
{
|
|
if (plane.active!= 1 ) {marker.set("labelStyle", { opacity: 0.66, 'background-color': 'red'}); return; }
|
|
if (plane.iMsg < 1 ) {marker.set("labelStyle", { opacity: 0.66, 'background-color': 'orange'}); return; }
|
|
if (plane.id == Selected) {marker.set("labelStyle", { opacity: 0.66, 'background-color': 'greenyellow'}); return; }
|
|
marker.set("labelStyle", {opacity: 0.6, 'background-color': 'LimeGreen'});
|
|
}
|
|
|
|
|
|
function fetchData() {
|
|
|
|
if ( Gmap.getBounds()==null) {
|
|
return;
|
|
}
|
|
|
|
var vps = Gmap.getBounds().getSouthWest().lat().toFixed(2);
|
|
var vpw = Gmap.getBounds().getSouthWest().lng().toFixed(2);
|
|
var vpn = Gmap.getBounds().getNorthEast().lat().toFixed(2);
|
|
var vpe = Gmap.getBounds().getNorthEast().lng().toFixed(2);
|
|
var viewpoint = '&vpn='+vpn+'&vpe='+vpe+'&vps='+vps+'&vpw='+vpw
|
|
|
|
var latcntr = Gmap.getCenter().lat().toFixed(2);
|
|
var loncntr = Gmap.getCenter().lng().toFixed(2);
|
|
|
|
var alt_min = "";
|
|
var alt_max = "";
|
|
if ( Filters.enable_alt ) {
|
|
if ( typeof Filters.altitude[0]!=="undefined" && Filters.altitude[0]!==null ) {
|
|
alt_min = "&altitude.from=" + Filters.altitude[0];
|
|
}
|
|
|
|
if ( typeof Filters.altitude[1]!=="undefined" && Filters.altitude[1]!==null ) {
|
|
alt_max = "&altitude.to=" + Filters.altitude[1];
|
|
}
|
|
}
|
|
|
|
getJSON(rcd_globals.fcgipath+'clients.json',
|
|
function(data_unfiltered) {
|
|
var stillhere = {}
|
|
var data = data_unfiltered["TLS"];
|
|
for (var j=0; j < data.length; j++) {
|
|
var plane = data[j];
|
|
var marker1 = null;
|
|
|
|
stillhere[plane.id] = true;
|
|
plane.fli = $.trim(plane.fli);
|
|
|
|
/* update */
|
|
if (Planes[plane.id]) {
|
|
marker1 = Planes[plane.id].marker1;
|
|
|
|
var newpos = new google.maps.LatLng(plane.lat, plane.lon);
|
|
|
|
/* update flight number */
|
|
marker1.setIcon(getIconForPlane(plane));
|
|
marker1.setPosition(newpos);
|
|
marker1.set("labelContent", labelDataTable(plane));
|
|
labelSetAnchor(plane, marker1, 12);
|
|
labelSetStyle(plane, marker1);
|
|
marker1.label.draw();
|
|
|
|
if (plane.id == Selected)
|
|
refreshSelectedInfo();
|
|
|
|
} else {
|
|
/* new flight */
|
|
marker1 = new MarkerWithLabel({
|
|
position: new google.maps.LatLng(plane.lat, plane.lon),
|
|
draggable: false,
|
|
raiseOnDrag: false,
|
|
zIndex: 70000,
|
|
map: Gmap,
|
|
labelContent: labelDataTable(plane),
|
|
labelAnchor: new google.maps.Point(0,0),
|
|
labelClass: "labels",
|
|
labelStyle: {},
|
|
labelZIndex: 60000,
|
|
icon: getIconForPlane(plane)
|
|
});
|
|
labelSetAnchor(plane,marker1,12);
|
|
labelSetStyle(plane,marker1);
|
|
|
|
/* Trap clicks for this marker. */
|
|
// marker1.addListener('click', selectPlane);
|
|
}
|
|
|
|
marker1.planehex = plane.id; /* for click */
|
|
plane.marker1 = marker1;
|
|
Planes[plane.id] = plane;
|
|
|
|
if (plane.fli.length == 0) {
|
|
marker1.setTitle(plane.id)
|
|
} else {
|
|
/* Popup window when mouse over aircraft icon */
|
|
var title = plane.reg+' ('+plane.id+')';
|
|
if (plane.org.length && plane.dst.length)
|
|
title += '\n'+plane.org+'>'+plane.dst;
|
|
marker1.setTitle(title)
|
|
}
|
|
}
|
|
NumPlanes = data.length;
|
|
|
|
/* Remove idle planes. */
|
|
for (var p in Planes) {
|
|
if (! stillhere[Planes[p].id]) {
|
|
if(Planes[p].id == Selected) {
|
|
deselectPlane();
|
|
}
|
|
Planes[p].marker1.setMap(null);
|
|
delete Planes[p];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
var CenterLat = 0.0;
|
|
var CenterLon = 0.0;
|
|
var DistanceUnit = 0;
|
|
var Zoomlevel = 7;
|
|
var MapStyle = "terrain";
|
|
$(document).ready(function() {
|
|
getJSON( rcd_globals.fcgipath+"param.json?param=ownlat,ownlon,distanceunit", function(params) {
|
|
CenterLat = params.ownlat;
|
|
CenterLon = params.ownlon;
|
|
DistanceUnit = params.distanceunit ? "km" : "nm";
|
|
initialize();
|
|
});
|
|
});
|
|
|
|
|
|
function initialize()
|
|
{
|
|
/* There is no info_box_table without refreshGeneralInfo() */
|
|
refreshGeneralInfo();
|
|
|
|
map_box = document.getElementById('map_box');
|
|
map_info_box = document.getElementById('map_info_box');
|
|
flight_info_box = document.getElementById('flight_info_box');
|
|
info_box_table = document.getElementById('info_box_table');
|
|
hide_info = document.getElementById('hide_info');
|
|
hide_info_btn = document.getElementById('hide_info_btn');
|
|
|
|
document.getElementById("btn_hide_info_box").onclick = toggleInfoBox;
|
|
|
|
my_cookie = getCookie('clients_gmap_init');
|
|
if (my_cookie) $.extend(GmapInit, JSON.parse(my_cookie));
|
|
|
|
Gmap = new google.maps.Map(map_box,{
|
|
disableDefaultUI: true,
|
|
center: new google.maps.LatLng(GmapInit.CenterLat, GmapInit.CenterLon),
|
|
zoom: GmapInit.Zoomlevel,
|
|
zoomControl: true,
|
|
zoomControlOptions: {
|
|
position: google.maps.ControlPosition.RIGHT_BOTTOM
|
|
},
|
|
scaleControl: false,
|
|
streetViewControl: false
|
|
});
|
|
|
|
// switch(MapStyle)
|
|
// {
|
|
// case "grey":
|
|
// Gmap.mapTypes.set('Grey map', rcmaptype1);
|
|
// Gmap.setMapTypeId('Grey map');
|
|
// break;
|
|
// case "terrain":
|
|
// Gmap.setMapTypeId(google.maps.MapTypeId.TERRAIN);
|
|
// break;
|
|
// case "roadmap":
|
|
// Gmap.setMapTypeId(google.maps.MapTypeId.ROADMAP);
|
|
// break;
|
|
// case "satellite":
|
|
// Gmap.setMapTypeId(google.maps.MapTypeId.SATELLITE);
|
|
// break;
|
|
// case "hybrid":
|
|
// Gmap.setMapTypeId(google.maps.MapTypeId.HYBRID);
|
|
// break;
|
|
// default:
|
|
// Gmap.setMapTypeId(google.maps.MapTypeId.TERRAIN);
|
|
// }
|
|
|
|
var currCntr = new google.maps.LatLng (GmapInit.CenterLat, GmapInit.CenterLon);
|
|
|
|
// Gmap.addListener('click', deselectPlane);
|
|
|
|
Gmap.addListener('zoom_changed', function() {
|
|
GmapInit.Zoomlevel=Gmap.getZoom();
|
|
setCookie('clients_gmap_init',JSON.stringify(GmapInit),9999);
|
|
});
|
|
|
|
Gmap.addListener('dragend', function() {
|
|
var currCntr = new google.maps.LatLng;
|
|
GmapInit.CenterLat = Gmap.getCenter().lat();
|
|
GmapInit.CenterLon = Gmap.getCenter().lng();
|
|
setCookie("pos", currCntr, 9999);
|
|
setCookie('clients_gmap_init',JSON.stringify(GmapInit),9999);
|
|
});
|
|
|
|
hideInfoBox();
|
|
|
|
var refreshLoop = function() {
|
|
return function() {
|
|
if (!typeof Filters.refreshtime !== "number") {
|
|
var timeout = 1000;
|
|
} else {
|
|
var timeout = Math.max(1000, 1000 * refreshtime);
|
|
}
|
|
window.setTimeout(refreshLoop, timeout);
|
|
if( rcd_globals.initialized ) {
|
|
fetchData();
|
|
}
|
|
}
|
|
}();
|
|
|
|
window.setTimeout(refreshLoop, 0);
|
|
|
|
|
|
}
|