require_relative '../models/visualization/collection' require_relative '../models/table/user_table' require_dependency 'carto/bounding_box_utils' require_dependency 'common/map_common' class Map < Sequel::Model include Carto::MapBoundaries self.raise_on_save_failure = false plugin :serialization, :json, :options one_to_many :tables, class: ::UserTable many_to_one :user many_to_many :layers, order: :order, after_add: proc { |map, layer| layer.after_added_to_map(map) } many_to_many :base_layers, clone: :layers, right_key: :layer_id many_to_many :carto_layers, clone: :layers, right_key: :layer_id, conditions: { kind: "carto" } many_to_many :data_layers, clone: :layers, right_key: :layer_id, conditions: "kind in ('carto', 'torque')" many_to_many :user_layers, clone: :layers, right_key: :layer_id, conditions: "kind in ('tiled', 'background', 'gmapsbase', 'wms')" many_to_many :torque_layers, clone: :layers, right_key: :layer_id, conditions: { kind: "torque" } many_to_many :other_layers, clone: :layers, right_key: :layer_id, conditions: "kind not in ('carto', 'tiled', 'background', 'gmapsbase', 'wms')" many_to_many :named_maps_layers, clone: :layers, right_key: :layer_id, conditions: "kind in ('tiled', 'background', 'gmapsbase', 'wms', 'carto')" plugin :association_dependencies, layers: :nullify PUBLIC_ATTRIBUTES = %w{ id user_id provider bounding_box_sw bounding_box_ne center zoom view_bounds_sw view_bounds_ne legends scrollwheel } # FE code, so (lat,lon) DEFAULT_OPTIONS = { zoom: 3, bounding_box_sw: [Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:miny], Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:minx]], bounding_box_ne: [Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:maxy], Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:maxx]], provider: 'leaflet', center: [30, 0] } attr_accessor :table_id, # Flag to detect if being destroyed by whom so invalidate_vizjson_varnish_cache skips it :being_destroyed_by_vis_id def before_save super self.updated_at = Time.now end def after_save super update_map_on_associated_entities notify_map_change end def notify_map_change visualization = visualizations.first force_notify_map_change end def force_notify_map_change update_related_named_maps invalidate_vizjson_varnish_cache end def before_destroy raise CartoDB::InvalidMember.new(user: "Viewer users can't destroy maps") if user && user.viewer layers.each(&:destroy) super invalidate_vizjson_varnish_cache end def public_values Hash[PUBLIC_ATTRIBUTES.map { |a| [a, send(a)] }] end def validate super errors.add(:user_id, "can't be blank") if user_id.blank? errors.add(:user, "Viewer users can't save maps") if user && user.viewer end def viz_updated_at latest_mapcap = visualizations.first.latest_mapcap latest_mapcap ? latest_mapcap.created_at : get_the_last_time_tiles_have_changed_to_render_it_in_vizjsons end def invalidate_vizjson_varnish_cache visualizations.each do |visualization| visualization.invalidate_cache unless visualization.id == being_destroyed_by_vis_id end end def update_related_named_maps visualizations.each do |visualization| visualization.save_named_map unless visualization.id == being_destroyed_by_vis_id end end def admits_layer?(layer) return admits_more_torque_layers? if layer.torque_layer? return admits_more_data_layers? if layer.data_layer? return admits_more_base_layers?(layer) if layer.base_layer? end def can_add_layer?(user, layer) return true if layer.base_layer? return false if user.max_layers && user.max_layers <= data_layers.count return false if user.viewer current_vis = visualizations.first current_vis.has_permission?(user, CartoDB::Visualization::Member::PERMISSION_READWRITE) end def visualizations @visualizations_collection ||= CartoDB::Visualization::Collection.new.fetch(map_id: [id]).to_a end def visualization visualizations.first end def process_privacy_in(layer) return self unless layer.uses_private_tables? visualizations.each do |visualization| if visualization.can_be_private? visualization.privacy = CartoDB::Visualization::Member::PRIVACY_PRIVATE visualization.store end end end def self.provider_for_baselayer_kind(kind) kind == 'tiled' ? 'leaflet' : 'googlemaps' end # (lat,lon) points on all map data def center_data center.presence || DEFAULT_OPTIONS[:center] end def view_bounds_data bbox_sw = view_bounds_sw.presence || DEFAULT_OPTIONS[:bounding_box_sw] bbox_ne = view_bounds_ne.presence || DEFAULT_OPTIONS[:bounding_box_ne] { # LowerCorner longitude, in decimal degrees west: bbox_sw[1], # LowerCorner latitude, in decimal degrees south: bbox_sw[0], # UpperCorner longitude, in decimal degrees east: bbox_ne[1], # UpperCorner latitude, in decimal degrees north: bbox_ne[0] } end def dup map = Map.new(to_hash.reject { |k, _| [:id, :options].include?(k) }) map.options = options # Manually copied to avoid serialization map end private def get_the_last_time_tiles_have_changed_to_render_it_in_vizjsons table = tables.first from_table = table.service.data_last_modified if table [from_table, data_layers.map(&:updated_at)].flatten.compact.max end def update_map_on_associated_entities return unless table_id # Cannot filter by user_id as might be a shared table not owned by us related_table = ::UserTable.filter(id: table_id).first if related_table.map_id != id # Manually propagate to visualization (@see Table.after_save) if exists (at table creation won't) if related_table.map_id.present? CartoDB::Visualization::Collection.new.fetch( user_id: user_id, map_id: related_table.map_id ).each { |entry| entry.store_from_map(map_id: id) } end # HERE BE DRAGONS! If we try to store using model, callbacks break hell. Manual update required related_table.this.update(map_id: id) end end def table_visualization? !!table_visualization end def table_visualization CartoDB::Visualization::Collection.new .fetch(map_id: [id], type: CartoDB::Visualization::Member::TYPE_CANONICAL) .first end def admits_more_data_layers? data_layers.length >= 1 && table_visualization? ? false : true end def admits_more_torque_layers? torque_layers.length >= 1 && table_visualization? ? false : true end def admits_more_base_layers?(layer) # no basemap layer, always allow return true if user_layers.length < 1 # have basemap? then allow only if comes on top (for labels) layer.order >= layers.last.order && user_layers.length >= 1 end def table_name tables.first.nil? ? nil : tables.first.name end end