opf/openproject

View on GitHub
modules/bim/app/contracts/bim/bcf/viewpoints/create_contract.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Bim::Bcf
  module Viewpoints
    class CreateContract < ::ModelContract
      include ::Bim::Bcf::Concerns::ManageBcfGuarded

      WHITELISTED_PROPERTIES = %w(guid
                                  index
                                  snapshot
                                  orthogonal_camera
                                  perspective_camera
                                  clipping_planes
                                  bitmaps
                                  lines
                                  components).freeze

      ORTHOGONAL_CAMERA_PROPERTIES = %w(camera_view_point
                                        camera_direction
                                        camera_up_vector
                                        view_to_world_scale).freeze

      PERSPECTIVE_CAMERA_PROPERTIES = %w(camera_view_point
                                         camera_direction
                                         camera_up_vector
                                         field_of_view).freeze

      LINES_PROPERTIES = %w(start_point
                            end_point).freeze

      CLIPPING_PLANES_PROPERTIES = %w(location
                                      direction).freeze

      COMPONENTS_PROPERTIES = %w(visibility
                                 selection
                                 coloring).freeze

      COMPONENT_PROPERTIES = %w(ifc_guid
                                originating_system
                                authoring_tool_id).freeze

      COLORING_PROPERTIES = %w(color
                               components).freeze

      VISIBILITY_PROPERTIES = %w(default_visibility
                                 exceptions
                                 view_setup_hints).freeze

      VIEW_SETUP_HINTS_PROPERTIES = %w(spaces_visible
                                       space_boundaries_visible
                                       openings_visible).freeze

      COLOR_REGEXP = /([0-9a-f]{2})?[0-9a-f]{6}/

      WHITELISTED_DIMENSIONS = %w(x y z).freeze

      attribute :uuid
      attribute :issue
      attribute :snapshot
      attribute :json_viewpoint do
        validate_json_viewpoint_blank
        validate_json_viewpoint_hash

        next if errors.any?

        validate_properties
        validate_snapshot
        validate_index
        validate_orthogonal_camera
        validate_perspective_camera
        validate_lines
        validate_clipping_planes
        validate_bitmaps
        validate_components
        validate_guid
      end

      def validate_json_viewpoint_blank
        errors.add(:json_viewpoint, :blank) if viewpoint.blank?
      end

      def validate_json_viewpoint_hash
        errors.add(:json_viewpoint, :no_json) if viewpoint.present? && !viewpoint.is_a?(Hash)
      end

      def validate_properties
        errors.add(:json_viewpoint, :unsupported_key) if viewpoint.present? && (viewpoint.keys - WHITELISTED_PROPERTIES).any?
      end

      def validate_snapshot
        return unless (sjson = viewpoint["snapshot"])

        errors.add(:json_viewpoint, :snapshot_type_unsupported) unless %w(jpg png).include? sjson["snapshot_type"]
        errors.add(:json_viewpoint, :snapshot_data_blank) if sjson["snapshot_data"].blank?
      end

      def validate_index
        return unless (ijson = viewpoint["index"])

        errors.add(:json_viewpoint, :index_not_integer) unless ijson.is_a? Integer
      end

      def validate_orthogonal_camera
        return unless (ocjson = viewpoint["orthogonal_camera"])

        if ocjson.keys != ORTHOGONAL_CAMERA_PROPERTIES ||
           ocjson.except("view_to_world_scale").any? { |_, direction| invalid_direction?(direction) } ||
           !ocjson["view_to_world_scale"].is_a?(Numeric)
          errors.add(:json_viewpoint, :invalid_orthogonal_camera)
        end
      end

      def validate_perspective_camera
        return unless (pcjson = viewpoint["perspective_camera"])

        if pcjson.keys != PERSPECTIVE_CAMERA_PROPERTIES ||
           pcjson.except("field_of_view").any? { |_, direction| invalid_direction?(direction) } ||
           !pcjson["field_of_view"].is_a?(Numeric)
          errors.add(:json_viewpoint, :invalid_perspective_camera)
        end
      end

      def validate_lines
        return unless (ljson = viewpoint["lines"])

        if !ljson.is_a?(Array) ||
           ljson.any? { |line| invalid_line?(line) }
          errors.add(:json_viewpoint, :invalid_lines)
        end
      end

      def validate_clipping_planes
        return unless (cpjson = viewpoint["clipping_planes"])

        if !cpjson.is_a?(Array) ||
           cpjson.any? { |cp| invalid_clipping_plane?(cp) }
          errors.add(:json_viewpoint, :invalid_clipping_planes)
        end
      end

      def validate_bitmaps
        errors.add(:json_viewpoint, :bitmaps_not_writable) if viewpoint["bitmaps"]
      end

      def validate_components
        return unless (cjson = viewpoint["components"])

        if !cjson.is_a?(Hash) ||
           invalid_components_properties?(cjson)
          errors.add(:json_viewpoint, :invalid_components)
        end
      end

      def validate_guid
        return unless (json_guid = viewpoint["guid"])

        errors.add(:json_viewpoint, :mismatching_guid) if json_guid != model.uuid
      end

      def invalid_components_properties?(json)
        (json.keys - COMPONENTS_PROPERTIES).any? ||
          invalid_visibility?(json["visibility"]) ||
          invalid_components?(json["selection"]) ||
          invalid_colorings?(json["coloring"])
      end

      def invalid_line?(line)
        invalid_hash_point?(line, LINES_PROPERTIES)
      end

      def invalid_clipping_plane?(line)
        invalid_hash_point?(line, CLIPPING_PLANES_PROPERTIES)
      end

      def invalid_hash_point?(hash, whitelist)
        !hash.is_a?(Hash) ||
          hash.keys != whitelist ||
          hash.values.any? { |v| invalid_point?(v) }
      end

      def invalid_visibility?(visibility)
        visibility.nil? ||
          !visibility.is_a?(Hash) ||
          (visibility.keys - VISIBILITY_PROPERTIES).any? ||
          invalid_default_visibility?(visibility["default_visibility"]) ||
          invalid_components?(visibility["exceptions"]) ||
          invalid_view_setup_hints?(visibility["view_setup_hints"])
      end

      def invalid_components?(components)
        return false if components.blank?

        !components.is_a?(Array) || components.any? { |component| invalid_component?(component) }
      end

      def invalid_colorings?(colorings)
        return false if colorings.blank?

        !colorings.is_a?(Array) || colorings.any? { |coloring| invalid_coloring?(coloring) }
      end

      def invalid_component?(component)
        !component.is_a?(Hash) ||
          component.empty? ||
          (component.keys - COMPONENT_PROPERTIES).any? ||
          component.values.any? { |v| !v.is_a?(String) }
      end

      def invalid_coloring?(coloring)
        !coloring.is_a?(Hash) ||
          coloring.keys != COLORING_PROPERTIES ||
          invalid_color?(coloring["color"]) ||
          invalid_components?(coloring["components"])
      end

      def invalid_color?(color)
        !(color.is_a?(String) && color.match?(COLOR_REGEXP))
      end

      def invalid_direction?(direction)
        !direction.is_a?(Hash) ||
          direction.keys != WHITELISTED_DIMENSIONS ||
          direction.values.any? { |v| !v.is_a? Numeric }
      end
      alias_method :invalid_point?, :invalid_direction?

      def invalid_default_visibility?(visibility)
        visibility.present? &&
          no_boolean?(visibility)
      end

      def invalid_view_setup_hints?(hints)
        return false if hints.nil?

        !hints.is_a?(Hash) ||
          (hints.keys - VIEW_SETUP_HINTS_PROPERTIES).any? ||
          hints.values.any? { |v| no_boolean?(v) }
      end

      def no_boolean?(property)
        !(property.is_a?(TrueClass) || property.is_a?(FalseClass))
      end

      def viewpoint
        model.json_viewpoint
      end
    end
  end
end