cartodb-4.42/app/controllers/carto/api/vizjson3_presenter.rb

444 lines
13 KiB
Ruby
Raw Normal View History

2024-04-06 13:25:13 +08:00
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 StandardError
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