<?php
declare(strict_types=1);
namespace MeinWeimar;
use WP_Post;
use WP_Term;
/**
* Central marker contract for all geo-aware clients.
*
* Marker assets are WordPress media. A category marker wins over a resource
* marker, which wins over the global fallback. Community markers are a
* separate opt-in contract and never inherit a public place marker.
*/
final class MapMarkers
{
public const TERM_META = 'mw_marker_media_id';
public static function hooks(): void
{
add_action('init', [self::class, 'register_meta']);
foreach (self::taxonomies() as $taxonomy) {
add_action($taxonomy . '_add_form_fields', [self::class, 'render_add_field']);
add_action($taxonomy . '_edit_form_fields', [self::class, 'render_edit_field'], 10, 2);
add_action('created_' . $taxonomy, [self::class, 'save_term']);
add_action('edited_' . $taxonomy, [self::class, 'save_term']);
}
}
public static function taxonomies(): array
{
$taxonomies = [];
foreach (ContentTypes::definitions() as $definition) {
if (!empty($definition['geo'])) {
$taxonomies[] = (string) $definition['category'];
}
}
return array_values(array_unique($taxonomies));
}
public static function register_meta(): void
{
foreach (self::taxonomies() as $taxonomy) {
register_term_meta($taxonomy, self::TERM_META, [
'single' => true,
'type' => 'integer',
'show_in_rest' => true,
'sanitize_callback' => static function ($value): int {
$id = absint($value);
return $id && wp_attachment_is_image($id) ? $id : 0;
},
'auth_callback' => static fn (): bool => current_user_can('manage_categories'),
]);
}
}
public static function render_add_field(string $taxonomy): void
{
echo '<div class="form-field term-mw-marker-wrap">';
echo '<label for="mw-marker-media-id">' . esc_html__('Kartenmarker', 'meinweimar-core') . '</label>';
self::render_picker_markup(0, 'mw-marker-media-id', 'mw-marker-media-preview');
echo '<p>' . esc_html__('Optionaler Marker für diese Kategorie. Er überschreibt den Ressourcentyp- und den globalen Fallback-Marker.', 'meinweimar-core') . '</p>';
echo '</div>';
}
public static function render_edit_field(WP_Term $term, string $taxonomy): void
{
$id = absint(get_term_meta($term->term_id, self::TERM_META, true));
echo '<tr class="form-field term-mw-marker-wrap">';
echo '<th scope="row"><label for="mw-marker-media-id">' . esc_html__('Kartenmarker', 'meinweimar-core') . '</label></th>';
echo '<td>';
self::render_picker_markup($id, 'mw-marker-media-id', 'mw-marker-media-preview');
echo '<p class="description">' . esc_html__('Optionaler Marker für diese Kategorie. Er überschreibt den Ressourcentyp- und den globalen Fallback-Marker.', 'meinweimar-core') . '</p>';
echo '</td></tr>';
}
public static function save_term(int $term_id): void
{
if (!current_user_can('manage_categories')) {
return;
}
$id = absint($_POST[self::TERM_META] ?? 0);
if ($id && wp_attachment_is_image($id)) {
update_term_meta($term_id, self::TERM_META, $id);
return;
}
delete_term_meta($term_id, self::TERM_META);
}
public static function for_post(WP_Post $post): array
{
if (!in_array($post->post_type, ContentTypes::geo_post_types(), true)) {
return self::payload(0, 'not-geo');
}
$taxonomy = ContentTypes::category_taxonomy($post->post_type);
if ($taxonomy) {
$terms = get_the_terms($post, $taxonomy);
if (is_array($terms)) {
foreach ($terms as $term) {
$id = absint(get_term_meta($term->term_id, self::TERM_META, true));
if ($id && wp_attachment_is_image($id)) {
return self::payload($id, 'category:' . $term->slug);
}
}
}
}
$settings = Settings::get();
$route = ContentTypes::route_for_post_type($post->post_type);
$resource_markers = (array) ($settings['geo_resource_markers'] ?? []);
$resource_id = absint($resource_markers[$route] ?? 0);
if ($resource_id && wp_attachment_is_image($resource_id)) {
return self::payload($resource_id, 'resource:' . $route);
}
return self::payload(absint($settings['geo_fallback_marker_id'] ?? 0), 'fallback');
}
public static function public_config(): array
{
$settings = Settings::get();
$resource_markers = [];
foreach (ContentTypes::definitions() as $route => $definition) {
if (!empty($definition['geo'])) {
$resource_markers[$route] = self::payload(
absint(((array) ($settings['geo_resource_markers'] ?? []))[$route] ?? 0),
'resource:' . $route
);
}
}
return [
'fallback' => self::payload(absint($settings['geo_fallback_marker_id'] ?? 0), 'fallback'),
'by_resource' => $resource_markers,
'user' => array_merge(
self::payload(absint($settings['geo_user_marker_id'] ?? 0), 'user'),
['kind' => 'user']
),
'community' => [
'enabled' => false,
'mode' => 'profile-image',
'requires_opt_in' => true,
'profile_image_visible_only_with_consent' => true,
'position_precision' => 'approximate',
],
];
}
private static function payload(int $id, string $source): array
{
if (!$id || !wp_attachment_is_image($id)) {
return [
'id' => 0,
'image_url' => '',
'srcset' => '',
'alt' => '',
'source' => $source,
];
}
return [
'id' => $id,
'image_url' => (string) wp_get_attachment_image_url($id, 'full'),
'app_url' => rest_url('meinweimar/v1/media/' . $id),
'srcset' => (string) (wp_get_attachment_image_srcset($id, 'full') ?: ''),
'alt' => (string) (get_post_meta($id, '_wp_attachment_image_alt', true) ?: get_the_title($id)),
'source' => $source,
];
}
private static function render_picker_markup(int $id, string $input_id, string $preview_id): void
{
$label = $id ? sprintf(__('Bild #%d ausgewählt', 'meinweimar-core'), $id) : __('Noch kein Marker ausgewählt', 'meinweimar-core');
echo '<input type="hidden" id="' . esc_attr($input_id) . '" name="' . esc_attr(self::TERM_META) . '" value="' . esc_attr((string) $id) . '">';
echo '<button type="button" class="button" data-mw-marker-picker data-input="' . esc_attr($input_id) . '" data-preview="' . esc_attr($preview_id) . '">' . esc_html__('Marker aus Mediathek wählen', 'meinweimar-core') . '</button> ';
echo '<span id="' . esc_attr($preview_id) . '">' . esc_html($label) . '</span>';
}
}
