PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.2
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.2
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.2, at src/Api/Transformers/TourTransformer.php

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