PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.0
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 8.5.4 All 221 releases
wpvr / src / Api / Transformers / TourTransformer.php

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

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