require_dependency 'carto/api/layer_vizjson_adapter' require_dependency 'carto/api/infowindow_migrator' require_dependency 'cartodb/redis_vizjson_cache' require_dependency 'carto/named_maps/template' require_dependency 'carto/legend_migrator' require_dependency 'carto/html_safe' module Carto module Api module ApiTemplates API_TEMPLATE_PUBLIC = 'public'.freeze API_TEMPLATE_PRIVATE = 'private'.freeze def api_templates_type(options) options.fetch(:https_request, false) ? API_TEMPLATE_PRIVATE : API_TEMPLATE_PUBLIC end end module DisplayVizjsonMode def display_named_map?(visualization, forced_privacy_version) forced_privacy_version != :force_anonymous && (visualization.retrieve_named_map? || forced_privacy_version == :force_named) end end class VizJSON3Presenter include ApiTemplates include DisplayVizjsonMode include Carto::HtmlSafe def initialize(visualization, redis_vizjson_cache = CartoDB::Visualization::RedisVizjsonCache.new($tables_metadata, 3)) @visualization = visualization @redis_vizjson_cache = redis_vizjson_cache end def to_vizjson(https_request: false) generate_vizjson(https_request: https_request, forced_privacy_version: nil) end def to_named_map_vizjson(https_request: false) generate_vizjson(https_request: https_request, forced_privacy_version: :force_named) end def to_anonymous_map_vizjson(https_request: false) generate_vizjson(https_request: https_request, forced_privacy_version: :force_anonymous) end private def generate_vizjson(https_request:, forced_privacy_version:) https_request ||= false version = case forced_privacy_version when :force_named '3n' when :force_anonymous '3a' else 3 end vizjson = if @redis_vizjson_cache @redis_vizjson_cache.cached(@visualization.id, https_request, version) do calculate_vizjson(https_request: https_request, forced_privacy_version: forced_privacy_version) end else calculate_vizjson(https_request: https_request, forced_privacy_version: forced_privacy_version) end vizjson end VIZJSON_VERSION = '3.0.0'.freeze def calculate_vizjson(https_request: false, forced_privacy_version: nil) options = { https_request: https_request } user = @visualization.user map = @visualization.map vizjson = { bounds: bounds_from(map), center: map.center, datasource: datasource_vizjson(options, forced_privacy_version), description: markdown_html_safe(@visualization.description), options: map_options(@visualization), id: @visualization.id, layers: layers_vizjson(forced_privacy_version), map_provider: map.provider, overlays: overlays_vizjson(@visualization), title: @visualization.qualified_name(user), updated_at: @visualization.updated_at, user: user_info_vizjson(user), version: VIZJSON_VERSION, widgets: widgets_vizjson, zoom: map.zoom } visualization_analyses = @visualization.analyses vizjson[:analyses] = if display_named_map?(@visualization, forced_privacy_version) visualization_analyses.map { |a| named_map_analysis_json(a) } else visualization_analyses.map(&:analysis_definition) end vizjson end def bounds_from(map) return nil unless map.view_bounds_sw && map.view_bounds_ne ::JSON.parse("[#{map.view_bounds_sw}, #{map.view_bounds_ne}]") rescue => e CartoDB::Logger.debug( message: "Error parsing map bounds: #{map.id}, #{map.view_bounds_sw}, #{map.view_bounds_ne}", exception: e) nil end def layers_vizjson(forced_privacy_version) ordered_layers = ( [basemap_layer] + @visualization.carto_layers + @visualization.other_layers + non_basemap_base_layers).compact if display_named_map?(@visualization, forced_privacy_version) named_map_vizjson_for_layers(ordered_layers) else anonymous_vizjson_for_layers(ordered_layers) end end def named_map_vizjson_for_layers(layers) layers.map do |layer| VizJSON3NamedMapLayerPresenter.new(layer, configuration).to_vizjson end end def anonymous_vizjson_for_layers(layers) layers.map do |layer| VizJSON3LayerPresenter.new(layer, configuration).to_vizjson end end def basemap_layer @visualization.user_layers.first end def non_basemap_base_layers base_layers = @visualization.user_layers base_layers.empty? ? [] : base_layers.slice(1, base_layers.length) end def named_map_analysis_json(analysis) analysis_definition_without_sources(analysis.analysis_definition) end def analysis_definition_without_sources(analysis_definition) if analysis_definition[:type] == 'source' analysis_definition.delete(:params) elsif analysis_definition[:params] && analysis_definition[:params][:source] analysis_definition_without_sources(analysis_definition[:params][:source]) end analysis_definition end def configuration Cartodb.config end def datasource_vizjson(options, forced_privacy_version) ds = { user_name: @visualization.user.username, maps_api_template: ApplicationHelper.maps_api_template(api_templates_type(options)), stat_tag: @visualization.id } if display_named_map?(@visualization, forced_privacy_version) ds[:template_name] = Carto::NamedMaps::Template.new(@visualization).name end ds end def user_info_vizjson(user) { fullname: user.name_or_username, avatar_url: user.avatar_url, profile_url: user.public_url } end def widgets_vizjson @visualization.widgets.map do |widget| Carto::Api::WidgetPresenter.new(widget).to_vizjson end end def map_options(visualization) map = visualization.map migration_options = { layer_selector: true } if visualization.overlays.any? { |o| o.type == 'layer_selector' } map.options.merge(migration_options || {}) end def overlays_vizjson(visualization) visualization.overlays. reject { |o| o.type == 'layer_selector' }. map { |o| Carto::Api::OverlayPresenter.new(o).to_vizjson } end end class VizJSON3NamedMapLayerPresenter include ApiTemplates def initialize(layer, configuration) @configuration = configuration @layer = layer end # Prepare a PORO (Hash object) for easy JSONification # @see https://github.com/CartoDB/carto.js/blob/privacy-maps/doc/vizjson_format.md def to_vizjson layer_vizjson = VizJSON3LayerPresenter.new(@layer, @configuration).to_vizjson if @layer.user_layer? layer_vizjson elsif layer_vizjson[:type] == 'torque' data_for_torque_layer(layer_vizjson) else data_for_carto_layer(layer_vizjson) end end private def data_for_torque_layer(layer_vizjson) layer_vizjson.delete(:sql) layer_vizjson end def data_for_carto_layer(layer_vizjson) layer_options = layer_vizjson[:options] data = { id: layer_vizjson[:id], type: layer_vizjson[:type], visible: layer_vizjson[:visible], options: { layer_name: layer_options[:layer_name], attribution: layer_options[:attribution], cartocss: layer_options[:cartocss] } } if layer_options && layer_options[:source] data[:options][:source] = layer_options[:source] end infowindow = layer_vizjson[:infowindow] if infowindow && infowindow['fields'] && !infowindow['fields'].empty? data[:infowindow] = infowindow end tooltip = layer_vizjson[:tooltip] if tooltip && tooltip['fields'] && !tooltip['fields'].empty? data[:tooltip] = tooltip end legend = layer_vizjson[:legend] if legend && legend['type'] != 'none' data[:legend] = legend end data[:legends] = layer_vizjson[:legends] || [] data end end class VizJSON3LayerPresenter include ApiTemplates include InfowindowMigrator EMPTY_CSS = '#dummy{}'.freeze TORQUE_ATTRS = %i( table_name user_name property blendmode resolution countby torque-duration torque-steps torque-blend-mode named_map visible attribution ).freeze INFOWINDOW_AND_TOOLTIP_KEYS = %w( fields template_name template template_type alternative_names width maxHeight headerColor ).freeze def initialize(layer, configuration = {}) @layer = layer @configuration = configuration end def to_vizjson vizjson = { id: @layer.id } if @layer.base_layer? vizjson.merge(as_base) else vizjson.merge(as_data).merge(@layer.torque? ? as_torque : as_carto) end end private def attribution @layer.user_tables.map(&:visualization).map(&:attributions).join(', ') end def as_data old_legend_not_migrated = @layer.legend && !@layer.legend[:migrated] if @layer.persisted? && old_legend_not_migrated @layer.legends.any? ? mark_old_legend_migrated : migrate_old_legend end legends_presentation = @layer.legends.map do |legend| Carto::Api::LegendPresenter.new(legend).to_hash end { legends: legends_presentation } end def migrate_old_legend Carto::LegendMigrator.new(@layer.id, @layer.legend).build.save mark_old_legend_migrated @layer.legends.reload end def mark_old_legend_migrated @layer.options[:legend][:migrated] = true @layer.save end def as_torque layer_options = @layer.options.deep_symbolize_keys torque_options = layer_options.select { |k| TORQUE_ATTRS.include? k } torque_options[:attribution] = attribution torque_options[:layer_name] = layer_name torque = { type: 'torque', options: torque_options } torque[:cartocss] = layer_options[:tile_style] torque[:cartocss_version] = layer_options[:style_version] torque[:sql] = sql_from(@layer) if @layer.options['source'].present? torque[:source] = @layer.options['source'] end sql_wrap = @layer.options['sql_wrap'] || @layer.options['query_wrapper'] torque[:sql_wrap] = sql_wrap if sql_wrap torque end def as_carto { type: 'CartoDB', infowindow: whitelisted_attrs(migrate_builder_infowindow(@layer.infowindow, mustache_dir: 'infowindows')), tooltip: whitelisted_attrs(migrate_builder_infowindow(@layer.tooltip, mustache_dir: 'tooltips')), visible: @layer.options['visible'], options: options_data } end def as_base { type: @layer.kind, options: @layer.options } end def options_data data = { layer_name: layer_name, cartocss: css_from(@layer.options), cartocss_version: @layer.options.fetch('style_version'), attribution: attribution } source = @layer.options['source'] if source data[:source] = source data.delete(:sql) else data[:sql] = sql_from(@layer) end sql_wrap = @layer.options['sql_wrap'] || @layer.options['query_wrapper'] data[:sql_wrap] = sql_wrap if sql_wrap data end def layer_name layer_alias = @layer.options.fetch('table_name_alias', nil) table_name = @layer.options.fetch('table_name') # .present? is not valid, as we want to allow whitespaced layer names. layer_alias.nil? || layer_alias.empty? ? table_name : layer_alias end def sql_from(layer) query = layer.options.fetch('query', '') query.present? ? query : layer.default_query(layer.visualization.user) end def css_from(options) style = options.include?('tile_style') ? options['tile_style'] : nil (style.nil? || style.strip.empty?) ? EMPTY_CSS : options.fetch('tile_style') end def public_options return @configuration if @configuration.empty? @configuration.fetch(:layer_opts).fetch("public_opts") end def whitelisted_attrs(infowindow_or_tooltip) if infowindow_or_tooltip infowindow_or_tooltip.select do |key, _| INFOWINDOW_AND_TOOLTIP_KEYS.include?(key) || INFOWINDOW_AND_TOOLTIP_KEYS.include?(key.to_s) end end end end end end