PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.0
9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 All 222 releases
wpvr / src / Api / Transformers / TourTransformer.php

TourTransformer.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.1.0, at src/Api/Transformers/TourTransformer.php

656 lines 37.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace RexTheme\WPVR\Api\Transformers;
4
5 use RexTheme\WPVR\Api\Contracts\TransformerInterface;
6
7 class TourTransformer implements TransformerInterface {
8
9 protected bool $is_pro;
10
11 public function __construct( bool $is_pro = false ) {
12 $this->is_pro = $is_pro;
13 }
14
15 /**
16 * Convert raw panodata PHP array → normalized API JSON shape.
17 */
18 public function toApi( array $raw ): array {
19 if ( ! $this->is_pro ) {
20 $raw = wpvr_get_effective_panodata( $raw );
21 }
22
23 $scenes = $this->scenes_to_api( $raw['panodata']['scene-list'] ?? [] );
24 // Enabled: check new key first, then fall back to legacy `autoRotate` speed (truthy = enabled).
25 $auto_rotate_enabled = ( $raw['autorotate-enabled'] ?? '' ) === 'on' || ! empty( $raw['autoRotate'] );
26 // Speed: new key first, then legacy `autoRotate` (which doubles as speed in old tours).
27 $auto_rotate_speed = isset( $raw['autorotate-speed'] )
28 ? (float) $raw['autorotate-speed']
29 : ( isset( $raw['autoRotate'] ) ? (float) $raw['autoRotate'] : -5 );
30 // Delay: new key first, then legacy `autoRotateInactivityDelay`.
31 $auto_rotate_delay = isset( $raw['autorotate-delay'] )
32 ? (int) $raw['autorotate-delay']
33 : ( isset( $raw['autoRotateInactivityDelay'] ) ? (int) $raw['autoRotateInactivityDelay'] : 2000 );
34 $auto_rotate = [
35 'enabled' => $auto_rotate_enabled,
36 'speed' => $auto_rotate_speed,
37 'delay' => $auto_rotate_delay,
38 'stopDelay' => isset( $raw['autorotationstopdelay'] ) && $raw['autorotationstopdelay'] !== ''
39 ? (int) $raw['autorotationstopdelay']
40 : ( isset( $raw['autoRotateStopDelay'] ) && $raw['autoRotateStopDelay'] !== '' ? (int) $raw['autoRotateStopDelay'] : null ),
41 ];
42
43 $default_scene_id = ! empty( $raw['defaultscene'] ) ? $raw['defaultscene'] : null;
44 $has_explicit_default_scene = ( $raw['defaultscene-auto'] ?? 'off' ) !== 'on';
45
46 return [
47 'defaultSceneId' => $has_explicit_default_scene ? $default_scene_id : null,
48 'tourType' => $this->detect_tour_type( $raw ),
49 'settings' => [
50 'autoLoad' => ! empty( $raw['autoLoad'] ),
51 'showControls' => isset( $raw['showControls'] ) ? (bool) $raw['showControls'] : true,
52 'previewText' => $raw['previewtext'] ?? '',
53 'previewImage' => $raw['preview'] ?? '',
54 'sceneFadeDuration' => isset( $raw['scenefadeduration'] ) ? (int) $raw['scenefadeduration'] : 0,
55 'showSceneInfo' => ( $raw['scene-info-enabled'] ?? 'on' ) !== 'off',
56 'autoRotate' => $auto_rotate,
57 ],
58 'floorPlan' => $this->floor_plan_to_api( $raw ),
59 'backgroundTour' => [
60 'enabled' => ( $raw['bg_tour_enabler'] ?? 'off' ) === 'on',
61 'title' => $raw['bg_tour_title'] ?? '',
62 'subtitle' => $raw['bg_tour_subtitle'] ?? '',
63 ],
64 'videoData' => [
65 'url' => $raw['vidurl'] ?? '',
66 'autoplay' => ( $raw['video-autoplay'] ?? $raw['autoplay'] ?? 'off' ) === 'on',
67 'loop' => ( $raw['video-loop'] ?? $raw['loop'] ?? 'off' ) === 'on',
68 ],
69 'streetViewData' => [
70 'embedUrl' => $raw['streetviewurl'] ?? '',
71 ],
72 'scenes' => $scenes,
73 'advancedSettings' => $this->advanced_settings_to_api( $raw ),
74 ];
75 }
76
77 /**
78 * Convert normalized API JSON → raw panodata PHP array.
79 */
80 public function fromApi( array $data ): array {
81 $settings = $data['settings'] ?? [];
82 $floor_plan = $data['floorPlan'] ?? [];
83 $bg_tour = $data['backgroundTour'] ?? [];
84 $auto_rotate = $settings['autoRotate'] ?? [];
85
86 $video_data = $data['videoData'] ?? [];
87 $street_view_data = $data['streetViewData'] ?? [];
88 $advanced_control = is_array( $data['advancedControl'] ?? null )
89 ? $data['advancedControl']
90 : ( is_array( $data['proData'] ?? null ) ? $data['proData'] : [] );
91 $tour_type = in_array( $data['tourType'] ?? 'image', [ 'image', 'video', 'street-view' ], true )
92 ? $data['tourType']
93 : 'image';
94 if ( ! $this->is_pro && $tour_type === 'street-view' ) {
95 $tour_type = 'image';
96 $street_view_data = [];
97 }
98
99 $scenes = is_array( $data['scenes'] ?? null ) ? $data['scenes'] : [];
100 $requested_default_scene_id = (string) ( $data['defaultSceneId'] ?? '' );
101 $has_explicit_default_scene = $requested_default_scene_id !== ''
102 && ! empty( array_filter(
103 $scenes,
104 static function ( $scene ) use ( $requested_default_scene_id ) {
105 return is_array( $scene ) && ( $scene['id'] ?? '' ) === $requested_default_scene_id;
106 }
107 ) );
108 $default_scene_id = $has_explicit_default_scene
109 ? $requested_default_scene_id
110 : ( $scenes[0]['id'] ?? '' );
111
112 $raw = [
113 'autoLoad' => ! empty( $settings['autoLoad'] ) && $settings['autoLoad'] !== 'off' && $settings['autoLoad'] !== 'false',
114 'showControls' => ! empty( $settings['showControls'] ),
115 'draggable' => ( isset( $advanced_control['draggable'] ) && ( $advanced_control['draggable'] === 'off' || $advanced_control['draggable'] === false ) ) ? 'off' : 'on',
116 'mouseZoom' => ( isset( $advanced_control['mouseZoom'] ) && ( $advanced_control['mouseZoom'] === 'off' || $advanced_control['mouseZoom'] === false ) ) ? 'off' : 'on',
117 'diskeyboard' => ( isset( $advanced_control['diskeyboard'] ) && ( $advanced_control['diskeyboard'] === 'off' || $advanced_control['diskeyboard'] === false ) ) ? 'on' : 'off',
118 'keyboardzoom' => ( isset( $advanced_control['keyboardzoom'] ) && ( $advanced_control['keyboardzoom'] === 'off' || $advanced_control['keyboardzoom'] === false ) ) ? false : true,
119 'previewtext' => $settings['previewText'] ?? '',
120 'scenefadeduration' => isset( $settings['sceneFadeDuration'] ) ? (string) $settings['sceneFadeDuration'] : '0',
121 'scene-info-enabled' => array_key_exists( 'showSceneInfo', $settings ) && ! $settings['showSceneInfo'] ? 'off' : 'on',
122 'defaultscene' => $default_scene_id,
123 'defaultscene-auto' => $has_explicit_default_scene ? 'off' : 'on',
124 'autorotate-enabled' => ! empty( $auto_rotate['enabled'] ) ? 'on' : 'off',
125 'autorotate-speed' => $auto_rotate['speed'] ?? -5,
126 'autorotate-delay' => $auto_rotate['delay'] ?? 2000,
127 'autorotationstopdelay' => $auto_rotate['stopDelay'] ?? '',
128 // Legacy keys for PRO plugin / shortcode compatibility.
129 'autoRotate' => ! empty( $auto_rotate['enabled'] ) ? ( $auto_rotate['speed'] ?? -5 ) : '',
130 'autoRotateInactivityDelay' => $auto_rotate['delay'] ?? 2000,
131 'autoRotateStopDelay' => $auto_rotate['stopDelay'] ?? '',
132 'floorplan-enabled' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
133 'floorplan-image' => $floor_plan['imageUrl'] ?? '',
134 'floorplan-compass' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
135 'floorplan-compass-color' => $floor_plan['directionIndicator']['color'] ?? '#6D28D9',
136 // Legacy keys — used by legacy rendering and pro plugin.
137 'floor_plan_tour_enabler' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
138 'floor_plan_attachment_url' => $floor_plan['imageUrl'] ?? '',
139 'floor_plan_custom_color' => ! empty( $floor_plan['pointerColor'] ) ? $floor_plan['pointerColor'] : '#cca92c',
140 'floor_plan_direction_indicator' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
141 'floor_plan_pointer_position' => $this->pointer_positions_from_api( $floor_plan ),
142 'floor_plan_data_list' => $this->pointer_data_list_from_api( $floor_plan ),
143 'bg_tour_enabler' => ! empty( $bg_tour['enabled'] ) ? 'on' : 'off',
144 'bg_tour_title' => $bg_tour['title'] ?? '',
145 'bg_tour_subtitle' => $bg_tour['subtitle'] ?? '',
146 'genericform' => ! empty( $advanced_control['genericform'] ) && $advanced_control['genericform'] !== 'off' ? 'on' : 'off',
147 'genericformshortcode' => sanitize_text_field( (string) ( $advanced_control['genericformshortcode'] ?? '' ) ),
148 'genericformicon' => sanitize_text_field( (string) ( $advanced_control['genericformicon'] ?? 'fab fa-wpforms' ) ),
149 'genericformiconcolor' => sanitize_hex_color( $advanced_control['genericformiconcolor'] ?? '' ) ?: '#f7fffb',
150 'calltoaction' => ! empty( $advanced_control['calltoaction'] ) && $advanced_control['calltoaction'] !== 'off' ? 'on' : 'off',
151 'buttontext' => sanitize_text_field( (string) ( $advanced_control['buttontext'] ?? 'Click Here' ) ),
152 'buttonurl' => esc_url_raw( (string) ( $advanced_control['buttonurl'] ?? '' ) ),
153 'button_configuration' => $this->button_configuration_to_api( $advanced_control['button_configuration'] ?? [] ),
154 'preview' => esc_url_raw( $settings['previewImage'] ?? '' ),
155 'panoid' => '',
156 'customcontrol' => $this->customcontrol_to_api(
157 is_array( $advanced_control['customcontrol'] ?? null )
158 ? $advanced_control['customcontrol']
159 : []
160 ),
161 'tour-type' => $tour_type,
162 'vidurl' => esc_url_raw( $video_data['url'] ?? '' ),
163 'video-autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
164 'video-loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
165 'autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
166 'loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
167 'vidid' => $tour_type === 'video' && ! empty( $video_data['url'] ) ? ( 'vid' . ( $data['tourId'] ?? $data['id'] ?? wp_rand( 1000, 99999 ) ) ) : '',
168 'streetviewurl' => esc_url_raw( $street_view_data['embedUrl'] ?? '' ),
169 'streetview' => ! empty( $street_view_data['embedUrl'] ) ? 'on' : 'off',
170 'panodata' => [
171 'scene-list' => $this->scenes_from_api(
172 $scenes,
173 $default_scene_id,
174 ( $settings['showSceneInfo'] ?? true ) !== false
175 ),
176 ],
177 ];
178
179 return $raw;
180 }
181
182 // -------------------------------------------------------------------------
183
184 private function floor_plan_to_api( array $raw ): array {
185 // Merge pointer positions and scene assignments into a unified array.
186 $positions = $raw['floor_plan_pointer_position'] ?? [];
187 $data_list = $raw['floor_plan_data_list'] ?? [];
188
189 // Build id → sceneId map from data_list ({id:'1', name:'plan1', value:'scene-id'}).
190 $scene_map = [];
191 foreach ( $data_list as $item ) {
192 $item = is_object( $item ) ? $item : (object) $item;
193 $scene_map[ $item->id ?? '' ] = $item->value ?? '';
194 }
195
196 $pointers = [];
197 foreach ( $positions as $pos ) {
198 $pos = is_object( $pos ) ? $pos : (object) $pos;
199 $pos_id = $pos->id ?? '';
200 // Extract numeric suffix from 'pointer-N'.
201 preg_match( '/(\d+)$/', $pos_id, $m );
202 $num = $m[1] ?? '';
203 $scene_id = $scene_map[ $num ] ?? $scene_map[ $pos_id ] ?? '';
204
205 $pointers[] = [
206 'id' => $pos_id,
207 'top' => $pos->data_top ?? '0%',
208 'left' => $pos->data_left ?? '0%',
209 'sceneId' => $scene_id,
210 ];
211 }
212
213 return [
214 // Read from new key first, fall back to legacy key.
215 'enabled' => ( $raw['floorplan-enabled'] ?? 'off' ) === 'on' || ( $raw['floor_plan_tour_enabler'] ?? 'off' ) === 'on',
216 'imageUrl' => $raw['floorplan-image'] ?? $raw['floor_plan_attachment_url'] ?? '',
217 'pointerColor' => ! empty( $raw['floor_plan_custom_color'] ) ? $raw['floor_plan_custom_color'] : '#cca92c',
218 'pointers' => $pointers,
219 'directionIndicator' => [
220 'enabled' => ( $raw['floorplan-compass'] ?? 'off' ) === 'on' || ( $raw['floor_plan_direction_indicator'] ?? 'off' ) === 'on',
221 'color' => $raw['floorplan-compass-color'] ?? '#6D28D9',
222 ],
223 ];
224 }
225
226 private function pointer_positions_from_api( array $floor_plan ): array {
227 $pointers = $floor_plan['pointers'] ?? [];
228 $color = $floor_plan['pointerColor'] ?? '#cca92c';
229 $result = [];
230 $index = 1;
231 foreach ( $pointers as $pointer ) {
232 $top = $pointer['top'] ?? '0%';
233 $left = $pointer['left'] ?? '0%';
234 $result[] = (object) [
235 'id' => 'pointer-' . $index,
236 'text' => (string) $index,
237 'data_top' => $top,
238 'data_left' => $left,
239 'style' => "background:{$color};top:{$top};left:{$left};",
240 ];
241 $index++;
242 }
243 return $result;
244 }
245
246 private function pointer_data_list_from_api( array $floor_plan ): array {
247 $pointers = $floor_plan['pointers'] ?? [];
248 $result = [];
249 $index = 1;
250 foreach ( $pointers as $pointer ) {
251 $result[] = (object) [
252 'id' => (string) $index,
253 'name' => 'plan' . $index,
254 'value' => $pointer['sceneId'] ?? '',
255 ];
256 $index++;
257 }
258 return $result;
259 }
260
261 /**
262 * Detect tour type supporting both new-UI ('tour-type' key) and legacy
263 * ('streetviewdata'/'vidid' keys) save formats.
264 */
265 private function detect_tour_type( array $raw ): string {
266 if ( ! empty( $raw['tour-type'] ) ) {
267 $allowed = [ 'image', 'video', 'street-view' ];
268 return in_array( $raw['tour-type'], $allowed, true ) ? $raw['tour-type'] : 'image';
269 }
270 if ( isset( $raw['streetviewdata'] ) || ! empty( $raw['streetviewurl'] ) ) {
271 return 'street-view';
272 }
273 if ( isset( $raw['vidid'] ) || ! empty( $raw['vidurl'] ) ) {
274 return 'video';
275 }
276 return 'image';
277 }
278
279 /**
280 * Extract pro advanced-control fields from raw panodata for the API response.
281 * tourLayout is stored as an array by pro but the React store uses a plain string.
282 */
283 protected function advanced_settings_to_api( array $raw ): array {
284 $b = static function ( $val, $default ) {
285 if ( is_bool( $val ) ) return $val;
286 return filter_var( $val ?? $default, FILTER_VALIDATE_BOOLEAN );
287 };
288
289 $tour_layout_raw = $raw['tourLayout'] ?? 'default';
290 $tour_layout = is_array( $tour_layout_raw )
291 ? ( $tour_layout_raw['layout'] ?? 'default' )
292 : (string) $tour_layout_raw;
293
294 // globalzoom is on when any of hfov/maxHfov/minHfov is set.
295 $globalzoom = ( ! empty( $raw['hfov'] ) || ! empty( $raw['maxHfov'] ) || ! empty( $raw['minHfov'] ) );
296
297 return [
298 'tourLayout' => $tour_layout,
299 'layout_icon_bg_color' => (string) ( $raw['layout_icon_bg_color'] ?? '#5a536e' ),
300 'layout_icon_color' => (string) ( $raw['layout_icon_color'] ?? '#ffffff' ),
301 'diskeyboard' => ! in_array( $raw['diskeyboard'] ?? 'off', [ 'on', 'true', true ], true ),
302 'keyboardzoom' => $b( $raw['keyboardzoom'] ?? null, true ),
303 'draggable' => $b( $raw['draggable'] ?? null, true ),
304 'mouseZoom' => $b( $raw['mouseZoom'] ?? null, true ),
305 'gyro' => $b( $raw['gyro'] ?? null, false ),
306 'deviceorientationcontrol' => $b( $raw['deviceorientationcontrol']?? null, false ),
307 'compass' => $b( $raw['compass'] ?? null, false ),
308 'vrgallery' => $b( $raw['vrgallery'] ?? null, false ),
309 'vrgallery_title' => $b( $raw['vrgallery_title'] ?? null, false ),
310 'vrgallery_icon_size' => $b( $raw['vrgallery_icon_size'] ?? null, false ),
311 'vrgallery_display' => $b( $raw['vrgallery_display'] ?? null, false ),
312 'scene_navigation' => $b( $raw['scene_navigation'] ?? null, false ),
313 'scene_navigation_content_type' => (string) ( $raw['scene_navigation_content_type'] ?? 'scene_id' ),
314 'sceneAnimation' => $b( $raw['sceneAnimation'] ?? null, false ),
315 'sceneAnimationName' => (string) ( $raw['sceneAnimationName'] ?? 'none' ),
316 'sceneAnimationTransitionDuration'=> (string) ( $raw['sceneAnimationTransitionDuration'] ?? '500' ),
317 'sceneAnimationTransitionDelay' => (string) ( $raw['sceneAnimationTransitionDelay'] ?? '0' ),
318 'bg_music' => $b( $raw['bg_music'] ?? null, false ),
319 'bg_music_url' => (string) ( $raw['bg_music_url'] ?? '' ),
320 'autoplay_bg_music' => $b( $raw['autoplay_bg_music'] ?? null, false ),
321 'loop_bg_music' => $b( $raw['loop_bg_music'] ?? null, false ),
322 'explainerSwitch' => $b( $raw['explainerSwitch'] ?? null, false ),
323 'explainerContent' => (string) ( $raw['explainerContent'] ?? '' ),
324 'cpLogoSwitch' => $b( $raw['cpLogoSwitch'] ?? null, false ),
325 'cpLogoImg' => (string) ( $raw['cpLogoImg'] ?? '' ),
326 'cpLogoContent' => (string) ( $raw['cpLogoContent'] ?? '' ),
327 'globalzoom' => $globalzoom,
328 'hfov' => isset( $raw['hfov'] ) && $raw['hfov'] !== '' ? (int) $raw['hfov'] : null,
329 'maxHfov' => isset( $raw['maxHfov'] ) && $raw['maxHfov'] !== '' ? (int) $raw['maxHfov'] : null,
330 'minHfov' => isset( $raw['minHfov'] ) && $raw['minHfov'] !== '' ? (int) $raw['minHfov'] : null,
331 'genericform' => ( ( $raw['genericform'] ?? 'off' ) === 'on' ),
332 'genericformshortcode' => (string) ( $raw['genericformshortcode'] ?? '' ),
333 'genericformicon' => (string) ( $raw['genericformicon'] ?? 'fab fa-wpforms' ),
334 'genericformiconcolor' => (string) ( $raw['genericformiconcolor'] ?? '#f7fffb' ),
335 'calltoaction' => ( ( $raw['calltoaction'] ?? 'off' ) === 'on' ),
336 'buttontext' => (string) ( $raw['buttontext'] ?? 'Click Here' ),
337 'buttonurl' => (string) ( $raw['buttonurl'] ?? '' ),
338 'button_configuration' => $this->button_configuration_to_api( $raw['button_configuration'] ?? [] ),
339 'customcss_enable' => ( ( $raw['customcss_enable'] ?? 'off' ) === 'on' ),
340 'customcss' => (string) ( $raw['customcss'] ?? '' ),
341 'customcontrol' => $this->customcontrol_to_api( $raw['customcontrol'] ?? [] ),
342 ];
343 }
344
345 private function customcontrol_to_api( $raw_ctrl ): array {
346 if ( ! is_array( $raw_ctrl ) ) {
347 $raw_ctrl = [];
348 }
349 $defaults = [
350 'panupSwitch' => 'off', 'panupColor' => '#f7fffb', 'panupIcon' => 'fas fa-angle-up',
351 'panDownSwitch' => 'off', 'panDownColor' => '#f7fffb', 'panDownIcon' => 'fas fa-angle-down',
352 'panLeftSwitch' => 'off', 'panLeftColor' => '#f7fffb', 'panLeftIcon' => 'fas fa-angle-left',
353 'panRightSwitch' => 'off', 'panRightColor' => '#f7fffb', 'panRightIcon' => 'fas fa-angle-right',
354 'panZoomInSwitch' => 'off', 'panZoomInColor' => '#f7fffb', 'panZoomInIcon' => 'fas fa-plus-circle',
355 'panZoomOutSwitch' => 'off', 'panZoomOutColor' => '#f7fffb', 'panZoomOutIcon' => 'fas fa-minus-circle',
356 'panFullscreenSwitch' => 'off', 'panFullscreenColor' => '#f7fffb', 'panFullscreenIcon' => 'fas fa-expand',
357 'gyroscopeSwitch' => 'off', 'gyroscopeColor' => '#f7fffb', 'gyroscopeIcon' => 'fas fa-dot-circle',
358 'backToHomeSwitch' => 'off', 'backToHomeColor' => '#f7fffb', 'backToHomeIcon' => 'fas fa-home',
359 'explainerColor' => '#f7fffb', 'explainerIcon' => 'fas fa-video',
360 ];
361 return array_merge( $defaults, array_intersect_key( $raw_ctrl, $defaults ) );
362 }
363
364 private function button_configuration_to_api( $raw_config ): array {
365 if ( ! is_array( $raw_config ) ) {
366 $raw_config = [];
367 }
368
369 $defaults = [
370 'button_open_new_tab' => 'off',
371 'button_background_color' => '#201cfe',
372 'button_font_color' => '#ffffff',
373 'button_font_size' => '14',
374 'button_font_weight' => '400',
375 'button_line_height' => '1',
376 'button_text_decoration' => 'none',
377 'button_transform' => 'none',
378 'button_alignment' => 'left',
379 'button_text_style' => 'normal',
380 'button_letter_spacing' => '1',
381 'button_word_spacing' => '0',
382 'button_border_width' => '1',
383 'button_border_style' => 'solid',
384 'button_border_color' => '#201cfe',
385 'button_border_radius' => '6',
386 'button_pt' => '10',
387 'button_pr' => '15',
388 'button_pb' => '10',
389 'button_pl' => '15',
390 ];
391 $config = array_merge( $defaults, array_intersect_key( $raw_config, $defaults ) );
392
393 $config['button_open_new_tab'] = in_array(
394 $config['button_open_new_tab'],
395 [ true, 1, '1', 'on' ],
396 true
397 ) ? 'on' : 'off';
398
399 $allowed_values = [
400 'button_font_weight' => [ '400', '500', '600', '700', '800', '900' ],
401 'button_text_decoration' => [ 'none', 'underline', 'overline', 'line-through' ],
402 'button_transform' => [ 'none', 'uppercase', 'lowercase', 'capitalize' ],
403 'button_alignment' => [ 'left', 'right', 'center', 'justified' ],
404 'button_text_style' => [ 'normal', 'italic', 'oblique' ],
405 'button_border_style' => [ 'solid', 'dashed', 'dotted', 'double', 'none' ],
406 ];
407 foreach ( $allowed_values as $key => $allowed ) {
408 $value = (string) $config[ $key ];
409 $config[ $key ] = in_array( $value, $allowed, true ) ? $value : $defaults[ $key ];
410 }
411
412 foreach ( [ 'button_background_color', 'button_font_color', 'button_border_color' ] as $key ) {
413 $value = (string) $config[ $key ];
414 $config[ $key ] = preg_match( '/^#[0-9a-fA-F]{6}$/', $value ) ? strtolower( $value ) : $defaults[ $key ];
415 }
416
417 foreach ( [
418 'button_font_size', 'button_line_height', 'button_letter_spacing',
419 'button_word_spacing', 'button_border_width', 'button_border_radius',
420 'button_pt', 'button_pr', 'button_pb', 'button_pl',
421 ] as $key ) {
422 $value = $config[ $key ];
423 $config[ $key ] = is_numeric( $value ) && (float) $value >= 0
424 ? (string) $value
425 : $defaults[ $key ];
426 }
427
428 return $config;
429 }
430
431 protected function scenes_to_api( array $scene_list ): array {
432 $n = static function ( $val, $cast ) {
433 if ( ! isset( $val ) || $val === '' ) return null;
434 return $cast === 'int' ? (int) $val : (float) $val;
435 };
436
437 $scenes = [];
438 foreach ( $scene_list as $scene ) {
439 $scenes[] = [
440 // Free fields
441 'id' => $scene['scene-id'] ?? '',
442 'name' => $scene['scene-ititle'] ?? '',
443 'type' => $scene['scene-type'] ?? 'equirectangular',
444 'imageUrl' => $scene['scene-attachment-url'] ?? '',
445 'isDefault' => ( $scene['dscene'] ?? 'off' ) === 'on',
446 'hotspots' => $this->hotspots_to_api( $scene['hotspot-list'] ?? [] ),
447 // Pro: metadata
448 'author' => $scene['scene-author'] ?? '',
449 'authorUrl' => $scene['scene-author-url'] ?? '',
450 'vaov' => $n( $scene['scene-vaov'] ?? null, 'int' ),
451 'haov' => $n( $scene['scene-haov'] ?? null, 'int' ),
452 'verticalOffset' => $n( $scene['scene-vertical-offset'] ?? null, 'float' ),
453 // Pro: default face orientation
454 'defaultFace' => $scene['ptyscene'] ?? 'off',
455 'pitch' => $n( $scene['scene-pitch'] ?? null, 'float' ),
456 'yaw' => $n( $scene['scene-yaw'] ?? null, 'float' ),
457 // Pro: vertical drag limits
458 'limitVertical' => $scene['cvgscene'] ?? 'off',
459 'maxPitch' => $n( $scene['scene-maxpitch'] ?? null, 'float' ),
460 'minPitch' => $n( $scene['scene-minpitch'] ?? null, 'float' ),
461 // Pro: horizontal drag limits
462 'limitHorizontal' => $scene['chgscene'] ?? 'off',
463 'maxYaw' => $n( $scene['scene-maxyaw'] ?? null, 'float' ),
464 'minYaw' => $n( $scene['scene-minyaw'] ?? null, 'float' ),
465 // Pro: per-scene zoom override
466 'customZoom' => $scene['czscene'] ?? 'off',
467 'zoom' => $n( $scene['scene-zoom'] ?? null, 'int' ),
468 'maxZoom' => $n( $scene['scene-maxzoom'] ?? null, 'int' ),
469 'minZoom' => $n( $scene['scene-minzoom'] ?? null, 'int' ),
470 // Pro: cubemap faces (6-element array, null when not set)
471 'cubemapFaces' => [
472 $scene['scene-attachment-url-face0'] ?? null,
473 $scene['scene-attachment-url-face1'] ?? null,
474 $scene['scene-attachment-url-face2'] ?? null,
475 $scene['scene-attachment-url-face3'] ?? null,
476 $scene['scene-attachment-url-face4'] ?? null,
477 $scene['scene-attachment-url-face5'] ?? null,
478 ],
479 ];
480 }
481 // Older tours did not store whether a hotspot entry point inherited
482 // the target scene face or was edited manually. Keep those links in
483 // inherit mode: only edits made through the new Navigation Entry Point
484 // controls explicitly record the custom mode.
485 foreach ( $scenes as &$scene ) {
486 foreach ( $scene['hotspots'] as &$hotspot ) {
487 if ( in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true ) ) {
488 continue;
489 }
490 $hotspot['sceneEntryPointMode'] = 'inherit';
491 }
492 unset( $hotspot );
493 }
494 unset( $scene );
495
496 return $scenes;
497 }
498
499 protected function scenes_from_api( array $scenes, string $default_scene_id = '', bool $show_scene_info = true ): array {
500 $scene_list = [];
501 $index = 1;
502 foreach ( $scenes as $scene ) {
503 $scene_id = $scene['id'] ?? '';
504 $faces = $scene['cubemapFaces'] ?? [];
505 $scene_data = [
506 // Free fields
507 'scene-id' => $scene_id,
508 'scene-ititle' => $scene['name'] ?? '',
509 'scene-type' => $this->is_pro && ( $scene['type'] ?? '' ) === 'cubemap' ? 'cubemap' : 'equirectangular',
510 'scene-attachment-url' => $scene['imageUrl'] ?? '',
511 'scene-show-info' => $show_scene_info ? 'on' : 'off',
512 'hotspot-list' => $this->hotspots_from_api( $scene['hotspots'] ?? [] ),
513 'dscene' => ( $default_scene_id !== '' && $scene_id === $default_scene_id ) ? 'on' : 'off',
514 ];
515
516 if ( $this->is_pro ) {
517 $scene_data = array_merge( $scene_data, [
518 // Pro: metadata
519 'scene-author' => $scene['author'] ?? '',
520 'scene-author-url' => $scene['authorUrl'] ?? '',
521 'scene-vaov' => $scene['vaov'] ?? '',
522 'scene-haov' => $scene['haov'] ?? '',
523 'scene-vertical-offset' => $scene['verticalOffset'] ?? '',
524 // Pro: default face orientation
525 'ptyscene' => $scene['defaultFace'] ?? 'off',
526 'scene-pitch' => $scene['pitch'] ?? '',
527 'scene-yaw' => $scene['yaw'] ?? '',
528 // Pro: vertical drag limits
529 'cvgscene' => $scene['limitVertical'] ?? 'off',
530 'scene-maxpitch' => $scene['maxPitch'] ?? '',
531 'scene-minpitch' => $scene['minPitch'] ?? '',
532 // Pro: horizontal drag limits
533 'chgscene' => $scene['limitHorizontal'] ?? 'off',
534 'scene-maxyaw' => $scene['maxYaw'] ?? '',
535 'scene-minyaw' => $scene['minYaw'] ?? '',
536 // Pro: per-scene zoom override
537 'czscene' => $scene['customZoom'] ?? 'off',
538 'scene-zoom' => $scene['zoom'] ?? '',
539 'scene-maxzoom' => $scene['maxZoom'] ?? '',
540 'scene-minzoom' => $scene['minZoom'] ?? '',
541 // Pro: cubemap faces
542 'scene-attachment-url-face0' => $faces[0] ?? '',
543 'scene-attachment-url-face1' => $faces[1] ?? '',
544 'scene-attachment-url-face2' => $faces[2] ?? '',
545 'scene-attachment-url-face3' => $faces[3] ?? '',
546 'scene-attachment-url-face4' => $faces[4] ?? '',
547 'scene-attachment-url-face5' => $faces[5] ?? '',
548 ] );
549 }
550
551 $scene_list[ $index ] = $scene_data;
552 $index++;
553 }
554 return $scene_list;
555 }
556
557 protected function hotspots_to_api( array $hotspot_list ): array {
558 $hotspots = [];
559 foreach ( $hotspot_list as $hotspot ) {
560 if ( empty( $hotspot['hotspot-title'] ) && empty( $hotspot['hotspot-type'] ) ) {
561 continue;
562 }
563 $scene_pitch = isset( $hotspot['hotspot-scene-pitch'] ) && $hotspot['hotspot-scene-pitch'] !== ''
564 ? (float) $hotspot['hotspot-scene-pitch']
565 : null;
566 $scene_yaw = isset( $hotspot['hotspot-scene-yaw'] ) && $hotspot['hotspot-scene-yaw'] !== ''
567 ? (float) $hotspot['hotspot-scene-yaw']
568 : null;
569 $hotspots[] = [
570 'id' => $hotspot['hotspot-id'] ?? wp_generate_uuid4(),
571 'type' => $hotspot['hotspot-type'] ?? 'info',
572 'pitch' => isset( $hotspot['hotspot-pitch'] ) ? (float) $hotspot['hotspot-pitch'] : 0,
573 'yaw' => isset( $hotspot['hotspot-yaw'] ) ? (float) $hotspot['hotspot-yaw'] : 0,
574 'text' => $hotspot['hotspot-title'] ?? '',
575 'content' => $hotspot['hotspot-content'] ?? '',
576 'url' => $hotspot['hotspot-url'] ?? '',
577 'urlOpen' => $hotspot['hotspot-url-open'] ?? 'off',
578 'hover' => $hotspot['hotspot-hover'] ?? '',
579 'targetSceneId' => $hotspot['hotspot-scene'] ?? '',
580 'customClass' => $hotspot['hotspot-customclass'] ?? '',
581 // Pro styling fields
582 'iconClass' => ( ( $hotspot['hotspot-customclass-pro'] ?? '' ) === 'none' || ( $hotspot['hotspot-customclass-pro'] ?? '' ) === '' ) ? '' : $hotspot['hotspot-customclass-pro'],
583 'iconBgColor' => $hotspot['hotspot-customclass-color-icon-value'] ?? '#00b4ff',
584 'iconColor' => $hotspot['hotspot-custom-icon-color-value'] ?? '#ffffff',
585 'blink' => $hotspot['hotspot-blink'] ?? 'on',
586 'shape' => $hotspot['hotspot-shape'] ?? 'round',
587 'border' => $hotspot['hotspot-border'] ?? 'off',
588 'borderWidth' => $hotspot['hotspot-border-width'] ?? '1',
589 'borderStyle' => $hotspot['hotspot-border-style'] ?? 'none',
590 'borderColor' => $hotspot['hotspot-border-color'] ?? '#00b4ff',
591 // Pro navigation entry point fields
592 'scenePitch' => $scene_pitch,
593 'sceneYaw' => $scene_yaw,
594 'sceneEntryPointMode' => $hotspot['hotspot-scene-entry-point-mode'] ?? null,
595 ];
596 }
597 return $hotspots;
598 }
599
600 protected function hotspots_from_api( array $hotspots ): array {
601 $hotspot_list = [];
602 foreach ( $hotspots as $hotspot ) {
603 $hotspot_type = sanitize_key( $hotspot['type'] ?? 'info' );
604 $is_fluent_form = $hotspot_type === 'fluent_form';
605 $raw_content = (string) ( $hotspot['content'] ?? '' );
606 $raw_hover = (string) ( $hotspot['hover'] ?? '' );
607
608 $content = function_exists( 'sanitize_content_preserve_styles' )
609 ? sanitize_content_preserve_styles( $raw_content, $is_fluent_form )
610 : wp_kses_post( $raw_content );
611 $hover = function_exists( 'sanitize_content_preserve_styles' )
612 ? sanitize_content_preserve_styles( $raw_hover, false )
613 : wp_kses_post( $raw_hover );
614
615 $hs = [
616 'hotspot-id' => $hotspot['id'] ?? wp_generate_uuid4(),
617 'hotspot-type' => $hotspot_type,
618 'hotspot-pitch' => (string) ( $hotspot['pitch'] ?? 0 ),
619 'hotspot-yaw' => (string) ( $hotspot['yaw'] ?? 0 ),
620 'hotspot-title' => sanitize_text_field( $hotspot['text'] ?? '' ),
621 'hotspot-content' => $content,
622 'hotspot-url' => sanitize_text_field( $hotspot['url'] ?? '' ),
623 'hotspot-url-open' => ( $hotspot['urlOpen'] ?? 'off' ) === 'on' ? 'on' : 'off',
624 'hotspot-hover' => $hover,
625 'hotspot-scene' => sanitize_text_field( $hotspot['targetSceneId'] ?? '' ),
626 'hotspot-customclass' => sanitize_text_field( $hotspot['customClass'] ?? '' ),
627 'hotspot-scene-list' => 'none',
628 ];
629
630 if ( $this->is_pro ) {
631 $hs = array_merge( $hs, [
632 // Pro styling fields
633 'hotspot-customclass-pro' => !empty( $hotspot['iconClass'] ) ? $hotspot['iconClass'] : 'none',
634 'hotspot-customclass-color-icon-value' => $hotspot['iconBgColor'] ?? '#00b4ff',
635 'hotspot-custom-icon-color-value' => $hotspot['iconColor'] ?? '#ffffff',
636 'hotspot-blink' => $hotspot['blink'] ?? 'on',
637 'hotspot-shape' => $hotspot['shape'] ?? 'round',
638 'hotspot-border' => $hotspot['border'] ?? 'off',
639 'hotspot-border-width' => $hotspot['borderWidth'] ?? '1',
640 'hotspot-border-style' => $hotspot['borderStyle'] ?? 'none',
641 'hotspot-border-color' => $hotspot['borderColor'] ?? '#00b4ff',
642 // Pro navigation entry point fields
643 'hotspot-scene-pitch' => $hotspot['scenePitch'] !== null ? (string) $hotspot['scenePitch'] : '',
644 'hotspot-scene-yaw' => $hotspot['sceneYaw'] !== null ? (string) $hotspot['sceneYaw'] : '',
645 'hotspot-scene-entry-point-mode' => in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true )
646 ? $hotspot['sceneEntryPointMode']
647 : 'inherit',
648 ] );
649 }
650
651 $hotspot_list[] = $hs;
652 }
653 return $hotspot_list;
654 }
655 }
656