More charts
This commit is contained in:
@@ -37,3 +37,24 @@
|
||||
flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */
|
||||
min-width: 300px; /* Zapobiega zbytniemu ściskaniu kolumn na mniejszych ekranach */
|
||||
}
|
||||
|
||||
/* Chart Controls */
|
||||
.mystat-chart-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.mystat-chart-tabs.nav-tab-wrapper {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mystat-xaxis-switcher {
|
||||
padding-top: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
+342
-206
@@ -774,113 +774,125 @@ function mystat_yearly_summary_page() {
|
||||
* @return array An array containing 'points' for the map and 'elevation_profile'.
|
||||
*/
|
||||
function mystat_parse_gpx_data( $gpx_url ) {
|
||||
if ( empty( $gpx_url ) ) {
|
||||
return [];
|
||||
}
|
||||
if ( empty( $gpx_url ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$response = wp_remote_get( $gpx_url, [ 'timeout' => 20 ] );
|
||||
$response = wp_remote_get( $gpx_url, [ 'timeout' => 20 ] );
|
||||
|
||||
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
return [];
|
||||
}
|
||||
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$gpx_content = wp_remote_retrieve_body( $response );
|
||||
if ( empty( $gpx_content ) ) {
|
||||
return [];
|
||||
}
|
||||
$gpx_content = wp_remote_retrieve_body( $response );
|
||||
if ( empty( $gpx_content ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// --- Privacy Zone ---
|
||||
$privacy_options = get_option( 'mystat_privacy_options' );
|
||||
$privacy_enabled = false;
|
||||
$privacy_center_lat = 0;
|
||||
$privacy_center_lon = 0;
|
||||
$privacy_radius_km = 0;
|
||||
// --- Privacy Zone ---
|
||||
$privacy_options = get_option( 'mystat_privacy_options' );
|
||||
$privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] );
|
||||
$privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0;
|
||||
$privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0;
|
||||
$privacy_radius_km = $privacy_enabled ? ( (int) $privacy_options['radius'] ) / 1000 : 0; // Convert meters to km
|
||||
|
||||
if ( ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] ) ) {
|
||||
$privacy_enabled = true;
|
||||
$privacy_center_lat = (float) $privacy_options['latitude'];
|
||||
$privacy_center_lon = (float) $privacy_options['longitude'];
|
||||
$privacy_radius_km = ( (int) $privacy_options['radius'] ) / 1000; // Convert meters to km
|
||||
}
|
||||
libxml_use_internal_errors( true );
|
||||
$gpx = simplexml_load_string( $gpx_content );
|
||||
libxml_clear_errors();
|
||||
|
||||
libxml_use_internal_errors( true );
|
||||
$gpx = simplexml_load_string( $gpx_content );
|
||||
libxml_clear_errors();
|
||||
if ( $gpx === false ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( $gpx === false ) {
|
||||
return [];
|
||||
}
|
||||
// Use XPath to be more robust against different GPX structures/namespaces
|
||||
$gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' );
|
||||
$trackpoints = $gpx->xpath( '//gpx:trkpt' );
|
||||
if ( empty( $trackpoints ) ) {
|
||||
$trackpoints = $gpx->xpath( '//trkpt' ); // Fallback for files without namespace
|
||||
}
|
||||
|
||||
$track_data = [
|
||||
'points' => [],
|
||||
'elevation_profile' => [],
|
||||
];
|
||||
$raw_points = [];
|
||||
$raw_points = [];
|
||||
$start_time = null;
|
||||
|
||||
// Extract raw points with lat, lon, ele
|
||||
if ( isset( $gpx->trk ) ) {
|
||||
foreach ( $gpx->trk->trkseg as $trkseg ) {
|
||||
foreach ( $trkseg->trkpt as $trkpt ) {
|
||||
if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) {
|
||||
$raw_points[] = [
|
||||
'lat' => (float) $trkpt['lat'],
|
||||
'lon' => (float) $trkpt['lon'],
|
||||
'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $trackpoints as $trkpt ) {
|
||||
if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) {
|
||||
$extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null;
|
||||
$time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null;
|
||||
if ( $time && is_null( $start_time ) ) {
|
||||
$start_time = $time;
|
||||
}
|
||||
|
||||
if ( empty( $raw_points ) ) {
|
||||
return $track_data;
|
||||
}
|
||||
$hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null;
|
||||
$cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null;
|
||||
|
||||
// Process raw points to calculate profile
|
||||
$cumulative_distance = 0;
|
||||
$elevation_profile = [];
|
||||
$map_points = [];
|
||||
$raw_points[] = [
|
||||
'lat' => (float) $trkpt['lat'],
|
||||
'lon' => (float) $trkpt['lon'],
|
||||
'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null,
|
||||
'time_offset' => $start_time && $time ? $time - $start_time : null,
|
||||
'hr' => $hr_val,
|
||||
'cad' => $cad_val,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$haversine = function( $lat1, $lon1, $lat2, $lon2 ) {
|
||||
$earth_radius = 6371; // in km
|
||||
$dLat = deg2rad( $lat2 - $lat1 );
|
||||
$dLon = deg2rad( $lon2 - $lon1 );
|
||||
$a = sin( $dLat / 2 ) * sin( $dLat / 2 ) +
|
||||
cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) *
|
||||
sin( $dLon / 2 ) * sin( $dLon / 2 );
|
||||
$c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) );
|
||||
return $earth_radius * $c;
|
||||
};
|
||||
if ( empty( $raw_points ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ( $raw_points as $i => $point ) {
|
||||
// Check if point is inside privacy zone
|
||||
$is_in_privacy_zone = false;
|
||||
if ( $privacy_enabled ) {
|
||||
$distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] );
|
||||
if ( $distance_from_center <= $privacy_radius_km ) {
|
||||
$is_in_privacy_zone = true;
|
||||
}
|
||||
}
|
||||
// Process raw points to calculate profiles
|
||||
$map_points = [];
|
||||
$profiles = [ 'distance' => [], 'time' => [], 'elevation' => [], 'speed' => [], 'hr' => [], 'cadence' => [] ];
|
||||
$cumulative_distance = 0;
|
||||
|
||||
// Only process points outside the privacy zone
|
||||
if ( ! $is_in_privacy_zone ) {
|
||||
$map_points[] = [ $point['lat'], $point['lon'] ];
|
||||
$haversine = function( $lat1, $lon1, $lat2, $lon2 ) {
|
||||
$earth_radius = 6371; // in km
|
||||
$dLat = deg2rad( $lat2 - $lat1 );
|
||||
$dLon = deg2rad( $lon2 - $lon1 );
|
||||
$a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * sin( $dLon / 2 ) * sin( $dLon / 2 );
|
||||
$c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) );
|
||||
return $earth_radius * $c;
|
||||
};
|
||||
|
||||
if ( $i > 0 ) {
|
||||
$prev_point = $raw_points[ $i - 1 ];
|
||||
$cumulative_distance += $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] );
|
||||
}
|
||||
foreach ( $raw_points as $i => $point ) {
|
||||
$is_in_privacy_zone = false;
|
||||
if ( $privacy_enabled ) {
|
||||
$distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] );
|
||||
if ( $distance_from_center <= $privacy_radius_km ) {
|
||||
$is_in_privacy_zone = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! is_null( $point['ele'] ) ) {
|
||||
$elevation_profile[] = [ 'distance' => round( $cumulative_distance, 3 ), 'elevation' => round( $point['ele'], 2 ) ];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! $is_in_privacy_zone ) {
|
||||
$map_points[] = [ $point['lat'], $point['lon'] ];
|
||||
$speed = null;
|
||||
|
||||
$track_data['points'] = $map_points;
|
||||
$track_data['elevation_profile'] = $elevation_profile;
|
||||
if ( $i > 0 ) {
|
||||
$prev_point = $raw_points[ $i - 1 ];
|
||||
$distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); // km
|
||||
$cumulative_distance += $distance_delta;
|
||||
|
||||
return $track_data;
|
||||
if ( ! is_null( $point['time_offset'] ) && ! is_null( $prev_point['time_offset'] ) ) {
|
||||
$time_delta = $point['time_offset'] - $prev_point['time_offset']; // seconds
|
||||
if ( $time_delta > 0 ) {
|
||||
$speed = ( $distance_delta * 3600 ) / $time_delta; // km/h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$profiles['distance'][] = round( $cumulative_distance, 3 );
|
||||
$profiles['time'][] = $point['time_offset'];
|
||||
$profiles['elevation'][] = ! is_null( $point['ele'] ) ? round( $point['ele'], 2 ) : null;
|
||||
$profiles['speed'][] = ! is_null( $speed ) ? round( $speed, 1 ) : null;
|
||||
$profiles['hr'][] = $point['hr'];
|
||||
$profiles['cadence'][] = $point['cad'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'points' => $map_points,
|
||||
'profiles' => $profiles,
|
||||
];
|
||||
}
|
||||
|
||||
function mystat_infographic_page() {
|
||||
@@ -1379,68 +1391,106 @@ function mystat_view_activity_page() {
|
||||
|
||||
// Prepare map and chart data if GPX exists
|
||||
$gpx_data = [];
|
||||
$has_gpx_data = false;
|
||||
if ( ! empty( $activity->gpx_url ) ) {
|
||||
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
|
||||
$has_gpx_data = !empty($gpx_data['points']);
|
||||
}
|
||||
|
||||
if ( ! empty( $gpx_data['points'] ) ) {
|
||||
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
|
||||
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
|
||||
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
||||
if ($has_gpx_data) {
|
||||
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
|
||||
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
|
||||
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
||||
|
||||
wp_register_script('mystat-details-loader', false);
|
||||
wp_enqueue_script('mystat-details-loader');
|
||||
wp_register_script('mystat-details-loader', false);
|
||||
wp_enqueue_script('mystat-details-loader');
|
||||
|
||||
$map_script = '
|
||||
const track_points = ' . json_encode($gpx_data['points']) . ';
|
||||
if (typeof L !== "undefined" && track_points.length > 0) {
|
||||
const map = L.map("mystat-activity-map");
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors\'
|
||||
}).addTo(map);
|
||||
const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map);
|
||||
map.fitBounds(polyline.getBounds().pad(0.1));
|
||||
L.marker(track_points[0]).addTo(map).bindPopup("Start");
|
||||
L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec");
|
||||
}
|
||||
';
|
||||
// Check which profiles have data
|
||||
$available_profiles = [];
|
||||
if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość';
|
||||
if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość';
|
||||
if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno';
|
||||
if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja';
|
||||
|
||||
$elevation_chart_script = '';
|
||||
if ( ! empty( $gpx_data['elevation_profile'] ) ) {
|
||||
$elevation_chart_script = '
|
||||
const elevation_data = ' . json_encode( $gpx_data['elevation_profile'] ) . ';
|
||||
if (typeof Chart !== "undefined" && elevation_data.length > 0) {
|
||||
const ctx = document.getElementById("mystat-elevation-chart").getContext("2d");
|
||||
new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: elevation_data.map(p => p.distance),
|
||||
datasets: [{
|
||||
label: "Wysokość (m)",
|
||||
data: elevation_data.map(p => p.elevation),
|
||||
borderColor: "' . esc_js( $activity->color ) . '",
|
||||
backgroundColor: "' . esc_js( $activity->color ) . '20",
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { title: { display: true, text: "Dystans (km)" } },
|
||||
y: { title: { display: true, text: "Wysokość (m n.p.m.)" } }
|
||||
},
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
}
|
||||
';
|
||||
$has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t)));
|
||||
|
||||
$chart_js = '
|
||||
const track_points = ' . json_encode($gpx_data['points']) . ';
|
||||
const profiles = ' . json_encode($gpx_data['profiles']) . ';
|
||||
let activeChart = null;
|
||||
|
||||
if (typeof L !== "undefined" && track_points.length > 0) {
|
||||
const map = L.map("mystat-activity-map");
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).addTo(map);
|
||||
const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map);
|
||||
map.fitBounds(polyline.getBounds().pad(0.1));
|
||||
L.marker(track_points[0]).addTo(map).bindPopup("Start");
|
||||
L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec");
|
||||
}
|
||||
|
||||
wp_add_inline_script('mystat-details-loader',
|
||||
'document.addEventListener("DOMContentLoaded", function() {' . $map_script . $elevation_chart_script . '});'
|
||||
);
|
||||
}
|
||||
const chartConfigs = {
|
||||
elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" },
|
||||
speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" },
|
||||
hr: { label: "Tętno", unit: "bpm", color: "#c0392b" },
|
||||
cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" }
|
||||
};
|
||||
const xAxisConfigs = {
|
||||
distance: { label: "Dystans (km)", data: profiles.distance },
|
||||
time: {
|
||||
label: "Czas", data: profiles.time,
|
||||
formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8)
|
||||
}
|
||||
};
|
||||
|
||||
function renderChart() {
|
||||
if (activeChart) activeChart.destroy();
|
||||
const chartType = document.querySelector(".mystat-chart-tabs .nav-tab-active").getAttribute("href").substring(1);
|
||||
const xAxisType = document.querySelector(\'input[name="mystat_xaxis"]:checked\').value;
|
||||
|
||||
const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data;
|
||||
const filteredY = [], filteredX = [];
|
||||
if(yData) {
|
||||
for(let i=0; i<yData.length; i++) {
|
||||
if (yData[i] !== null) {
|
||||
filteredY.push(yData[i]);
|
||||
filteredX.push(xData[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = document.getElementById("mystat-details-chart").getContext("2d");
|
||||
activeChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: filteredX,
|
||||
datasets: [{
|
||||
label: chartConfigs[chartType].label, data: filteredY,
|
||||
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
|
||||
fill: true, pointRadius: 0, tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => v, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } },
|
||||
y: { title: { display: true, text: chartConfigs[chartType].unit } }
|
||||
},
|
||||
plugins: { legend: { display: false } },
|
||||
interaction: { intersect: false, mode: "index" },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".mystat-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
document.querySelector(".mystat-chart-tabs .nav-tab-active").classList.remove("nav-tab-active");
|
||||
e.target.classList.add("nav-tab-active");
|
||||
renderChart();
|
||||
}));
|
||||
document.querySelectorAll(\'input[name="mystat_xaxis"]\').forEach(r => r.addEventListener("change", renderChart));
|
||||
if (document.querySelector(".mystat-chart-tabs .nav-tab")) renderChart();
|
||||
';
|
||||
wp_add_inline_script('mystat-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});');
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -1498,23 +1548,43 @@ function mystat_view_activity_page() {
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
|
||||
<?php if ( ! empty( $gpx_data['points'] ) ) : ?>
|
||||
<?php if ( $has_gpx_data ) : ?>
|
||||
<hr>
|
||||
<h3>Mapa Trasy</h3>
|
||||
<div id="mystat-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div>
|
||||
|
||||
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?>
|
||||
<h3>Profil Wysokości</h3>
|
||||
<div style="position: relative; height:250px; width:100%;">
|
||||
<canvas id="mystat-elevation-chart"></canvas>
|
||||
<?php if ( ! empty( $available_profiles ) ) : ?>
|
||||
<h3>Wykresy</h3>
|
||||
<div class="mystat-charts-container">
|
||||
<div class="mystat-chart-controls">
|
||||
<nav class="nav-tab-wrapper mystat-chart-tabs">
|
||||
<?php
|
||||
$is_first = true;
|
||||
foreach ( $available_profiles as $key => $label ) : ?>
|
||||
<a href="#<?php echo esc_attr($key); ?>" class="nav-tab <?php if ($is_first) { echo 'nav-tab-active'; $is_first = false; } ?>"><?php echo esc_html($label); ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php if ($has_time_data): ?>
|
||||
<div class="mystat-xaxis-switcher">
|
||||
<strong>Oś X:</strong>
|
||||
<label><input type="radio" name="mystat_xaxis" value="distance" checked> Dystans</label>
|
||||
|
||||
<label><input type="radio" name="mystat_xaxis" value="time"> Czas</label>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="mystat_xaxis" value="distance" checked>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="position: relative; height:250px; width:100%;">
|
||||
<canvas id="mystat-details-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ( ! empty( $activity->gpx_url ) ) : ?>
|
||||
<hr>
|
||||
<h3>Mapa Trasy</h3>
|
||||
<div class="notice notice-warning inline">
|
||||
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty.</p>
|
||||
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -2142,84 +2212,129 @@ function mystat_single_activity_shortcode_handler( $atts ) {
|
||||
|
||||
ob_start();
|
||||
|
||||
// Prepare map data if GPX exists
|
||||
// Prepare map and chart data if GPX exists
|
||||
$gpx_data = [];
|
||||
$has_gpx_data = false;
|
||||
if ( ! empty( $activity->gpx_url ) ) {
|
||||
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
|
||||
$has_gpx_data = !empty($gpx_data['points']);
|
||||
}
|
||||
|
||||
if ( ! empty( $gpx_data['points'] ) ) {
|
||||
// Enqueue scripts for the frontend
|
||||
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
|
||||
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
|
||||
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
||||
$unique_id = 'mystat-activity-' . esc_attr( $activity->id );
|
||||
|
||||
$map_id = 'mystat-map-' . esc_attr( $activity->id );
|
||||
$chart_id = 'mystat-chart-' . esc_attr( $activity->id );
|
||||
if ($has_gpx_data) {
|
||||
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
|
||||
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
|
||||
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
||||
|
||||
// Pass track data to a script
|
||||
wp_register_script('mystat-shortcode-loader-' . $activity->id, false);
|
||||
wp_enqueue_script('mystat-shortcode-loader-' . $activity->id);
|
||||
$map_id = 'map-' . $unique_id;
|
||||
$chart_id = 'chart-' . $unique_id;
|
||||
|
||||
$available_profiles = [];
|
||||
if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość';
|
||||
if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość';
|
||||
if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno';
|
||||
if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja';
|
||||
|
||||
$has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t)));
|
||||
|
||||
wp_register_script('mystat-shortcode-loader-' . $activity->id, false);
|
||||
wp_enqueue_script('mystat-shortcode-loader-' . $activity->id);
|
||||
|
||||
$js_script = '
|
||||
(function() {
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const uniqueId = "' . esc_js($unique_id) . '";
|
||||
const containerEl = document.getElementById(uniqueId);
|
||||
if (!containerEl || typeof L === "undefined" || typeof Chart === "undefined") return;
|
||||
|
||||
$map_script = '
|
||||
const trackPoints = ' . json_encode( $gpx_data['points'] ) . ';
|
||||
const profiles = ' . json_encode( $gpx_data['profiles'] ) . ';
|
||||
let activeChart = null;
|
||||
|
||||
const mapId = "' . esc_js($map_id) . '";
|
||||
const mapEl = document.getElementById(mapId);
|
||||
if (mapEl && trackPoints.length > 0) {
|
||||
if (mapEl._leaflet_id) mapEl._leaflet_id = null;
|
||||
const map = L.map(mapId);
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).addTo(map);
|
||||
const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map);
|
||||
map.fitBounds(polyline.getBounds().pad(0.1));
|
||||
}
|
||||
|
||||
var container = L.DomUtil.get(mapId);
|
||||
if(container != null) { container._leaflet_id = null; }
|
||||
const chartId = "' . esc_js($chart_id) . '";
|
||||
const chartEl = document.getElementById(chartId);
|
||||
if (!chartEl) return;
|
||||
|
||||
const map = L.map(mapId);
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\'
|
||||
}).addTo(map);
|
||||
const chartConfigs = {
|
||||
elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" },
|
||||
speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" },
|
||||
hr: { label: "Tętno", unit: "bpm", color: "#c0392b" },
|
||||
cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" }
|
||||
};
|
||||
const xAxisConfigs = {
|
||||
distance: { label: "Dystans (km)", data: profiles.distance },
|
||||
time: { label: "Czas", data: profiles.time, formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) }
|
||||
};
|
||||
|
||||
const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map);
|
||||
map.fitBounds(polyline.getBounds().pad(0.1));
|
||||
';
|
||||
function renderChart() {
|
||||
if (activeChart) activeChart.destroy();
|
||||
const activeTab = containerEl.querySelector(".mystat-chart-tab.active");
|
||||
if (!activeTab) return;
|
||||
const chartType = activeTab.dataset.type;
|
||||
const xAxisRadio = containerEl.querySelector(\'input[name="xaxis-\' + uniqueId + \'"]:checked\');
|
||||
if (!xAxisRadio) return;
|
||||
const xAxisType = xAxisRadio.value;
|
||||
|
||||
$elevation_chart_script = '';
|
||||
if ( ! empty( $gpx_data['elevation_profile'] ) ) {
|
||||
$elevation_chart_script = '
|
||||
const elevationData = ' . json_encode( $gpx_data['elevation_profile'] ) . ';
|
||||
const chartId = "' . esc_js($chart_id) . '";
|
||||
const ctx = document.getElementById(chartId).getContext("2d");
|
||||
new Chart(ctx, {
|
||||
const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data;
|
||||
const filteredY = [], filteredX = [];
|
||||
if(yData) {
|
||||
for(let i=0; i<yData.length; i++) {
|
||||
if (yData[i] !== null) {
|
||||
filteredY.push(yData[i]);
|
||||
filteredX.push(xData[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = chartEl.getContext("2d");
|
||||
activeChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: elevationData.map(p => p.distance),
|
||||
labels: filteredX,
|
||||
datasets: [{
|
||||
label: "Wysokość (m)",
|
||||
data: elevationData.map(p => p.elevation),
|
||||
borderColor: "' . esc_js( $activity->category_color ) . '",
|
||||
backgroundColor: "' . esc_js( $activity->category_color ) . '20",
|
||||
label: chartConfigs[chartType].label, data: filteredY,
|
||||
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
|
||||
fill: true, pointRadius: 0, tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { title: { display: true, text: "Dystans (km)" } },
|
||||
y: { title: { display: true, text: "Wysokość (m n.p.m.)" } }
|
||||
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => v, maxRotation: 0, autoSkip: true, maxTicksLimit: 7 } },
|
||||
y: { title: { display: true, text: chartConfigs[chartType].unit } }
|
||||
},
|
||||
plugins: { legend: { display: false } }
|
||||
plugins: { legend: { display: false } },
|
||||
interaction: { intersect: false, mode: "index" },
|
||||
}
|
||||
});
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
wp_add_inline_script('mystat-shortcode-loader-' . $activity->id,
|
||||
'(function() {
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof L === "undefined" || typeof Chart === "undefined") return;
|
||||
' . $map_script . '
|
||||
' . $elevation_chart_script . '
|
||||
});
|
||||
})();'
|
||||
);
|
||||
}
|
||||
containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active");
|
||||
e.currentTarget.classList.add("active");
|
||||
renderChart();
|
||||
}));
|
||||
containerEl.querySelectorAll(\'input[name="xaxis-\' + uniqueId + \'"]\').forEach(r => r.addEventListener("change", renderChart));
|
||||
if (containerEl.querySelector(".mystat-chart-tab")) renderChart();
|
||||
});
|
||||
})();';
|
||||
wp_add_inline_script('mystat-shortcode-loader-' . $activity->id, $js_script);
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="mystat-single-activity-shortcode">
|
||||
<div class="mystat-single-activity-shortcode" id="<?php echo esc_attr($unique_id); ?>">
|
||||
<h4><?php echo esc_html( $activity->title ); ?></h4>
|
||||
<p><em><?php echo esc_html( date_i18n( 'j F Y', strtotime( $activity->date ) ) ); ?></em></p>
|
||||
|
||||
@@ -2247,14 +2362,35 @@ function mystat_single_activity_shortcode_handler( $atts ) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ( ! empty( $gpx_data['points'] ) ) : ?>
|
||||
<div id="<?php echo $map_id; ?>" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
|
||||
<?php endif; ?>
|
||||
<?php if ( $has_gpx_data ) : ?>
|
||||
<div id="<?php echo esc_attr($map_id); ?>" class="mystat-single-map" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
|
||||
|
||||
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?>
|
||||
<div style="position: relative; height:250px; width:100%;">
|
||||
<canvas id="<?php echo $chart_id; ?>"></canvas>
|
||||
</div>
|
||||
<?php if ( ! empty( $available_profiles ) ) : ?>
|
||||
<div class="mystat-charts-container">
|
||||
<div class="mystat-chart-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; gap: 10px;">
|
||||
<div class="mystat-chart-tabs" style="display: flex; flex-wrap: wrap; gap: 5px;">
|
||||
<?php
|
||||
$is_first = true;
|
||||
foreach ( $available_profiles as $key => $label ) : ?>
|
||||
<button data-type="<?php echo esc_attr($key); ?>" class="mystat-chart-tab <?php if ($is_first) { echo 'active'; $is_first = false; } ?>"><?php echo esc_html($label); ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ($has_time_data): ?>
|
||||
<div class="mystat-xaxis-switcher" style="padding-top: 5px; font-size: 0.9em; white-space: nowrap;">
|
||||
<strong>Oś X:</strong>
|
||||
<label><input type="radio" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="distance" checked> Dystans</label>
|
||||
|
||||
<label><input type="radio" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="time"> Czas</label>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="xaxis-<?php echo esc_attr($unique_id); ?>" value="distance" checked>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mystat-chart-wrapper" style="position: relative; height:250px; width:100%;">
|
||||
<canvas id="<?php echo esc_attr($chart_id); ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user