第08章-标注与标签图层

第08章:标注与标签图层

8.1 LabelLayer 基础

8.1.1 创建标注图层

using SharpMap.Layers;
using SharpMap.Data.Providers;
using SharpMap.Styles;

// 创建标注图层
var labelLayer = new LabelLayer("城市标注");

// 设置数据源(通常与矢量图层共享)
labelLayer.DataSource = cityLayer.DataSource;

// 设置标注字段
labelLayer.LabelColumn = "CITY_NAME";

// 设置样式
labelLayer.Style = new LabelStyle
{
    Font = new Font("Microsoft YaHei", 10, FontStyle.Bold),
    ForeColor = Color.Black,
    CollisionDetection = true
};

// 启用
labelLayer.Enabled = true;

// 添加到地图
map.Layers.Add(labelLayer);

8.1.2 标注字段配置

// 使用单个字段
labelLayer.LabelColumn = "NAME";

// 使用委托组合多个字段
labelLayer.LabelStringDelegate = (row) =>
{
    string name = row["NAME"]?.ToString() ?? "";
    string code = row["CODE"]?.ToString() ?? "";
    return $"{name}\n({code})";
};

// 根据条件显示不同内容
labelLayer.LabelStringDelegate = (row) =>
{
    double population = Convert.ToDouble(row["POPULATION"] ?? 0);
    string name = row["NAME"]?.ToString() ?? "";
    
    if (population > 1000000)
    {
        return $"{name}\n人口: {population / 10000:N0}万";
    }
    return name;
};

// 格式化数值
labelLayer.LabelStringDelegate = (row) =>
{
    string name = row["NAME"]?.ToString() ?? "";
    double area = Convert.ToDouble(row["AREA"] ?? 0);
    return $"{name}\n{area:N2} km²";
};

8.2 标注样式详解

8.2.1 字体设置

// 基本字体
labelLayer.Style.Font = new Font("Arial", 12);

// 粗体
labelLayer.Style.Font = new Font("Arial", 12, FontStyle.Bold);

// 斜体
labelLayer.Style.Font = new Font("Arial", 12, FontStyle.Italic);

// 组合样式
labelLayer.Style.Font = new Font("Arial", 12, FontStyle.Bold | FontStyle.Italic);

// 中文字体
labelLayer.Style.Font = new Font("Microsoft YaHei", 11, FontStyle.Regular);
labelLayer.Style.Font = new Font("SimSun", 10, FontStyle.Regular);
labelLayer.Style.Font = new Font("KaiTi", 12, FontStyle.Regular);

8.2.2 颜色和背景

// 前景色
labelLayer.Style.ForeColor = Color.Black;
labelLayer.Style.ForeColor = Color.FromArgb(50, 50, 50);

// 背景色(标注框)
labelLayer.Style.BackColor = new SolidBrush(Color.White);
labelLayer.Style.BackColor = new SolidBrush(Color.FromArgb(200, 255, 255, 200));

// 透明背景
labelLayer.Style.BackColor = null;

// 光晕效果(描边)
labelLayer.Style.Halo = new Pen(Color.White, 2);
labelLayer.Style.Halo = new Pen(Color.FromArgb(200, 255, 255, 255), 3);

8.2.3 对齐和偏移

// 水平对齐
labelLayer.Style.HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Left;
labelLayer.Style.HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center;
labelLayer.Style.HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Right;

// 垂直对齐
labelLayer.Style.VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Top;
labelLayer.Style.VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Middle;
labelLayer.Style.VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Bottom;

// 偏移(像素)
labelLayer.Style.Offset = new PointF(0, -15);  // 向上偏移
labelLayer.Style.Offset = new PointF(10, 0);   // 向右偏移
labelLayer.Style.Offset = new PointF(5, -10);  // 右上偏移

// 常用组合:点符号上方显示标注
labelLayer.Style.HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center;
labelLayer.Style.VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Bottom;
labelLayer.Style.Offset = new PointF(0, -12);

8.3 碰撞检测

8.3.1 基本碰撞检测

// 启用碰撞检测
labelLayer.Style.CollisionDetection = true;

// 设置碰撞缓冲区
labelLayer.Style.CollisionBuffer = new SizeF(5, 5);  // 5 像素的缓冲
labelLayer.Style.CollisionBuffer = new SizeF(10, 3); // 水平 10,垂直 3

8.3.2 优先级控制

// 使用属性字段作为优先级
labelLayer.PriorityColumn = "PRIORITY";

// 使用人口作为优先级(人口多的优先显示)
labelLayer.PriorityColumn = "POPULATION";

// 使用委托计算优先级
labelLayer.GetPriorityDelegate = (row) =>
{
    // 首都最高优先级
    if (row["IS_CAPITAL"]?.ToString() == "Y")
        return 100;
    
    // 省会次之
    if (row["IS_PROVINCIAL"]?.ToString() == "Y")
        return 80;
    
    // 根据人口计算优先级
    double population = Convert.ToDouble(row["POPULATION"] ?? 0);
    return (int)(population / 100000);
};

8.3.3 多点位置尝试

// 自定义标注位置选择器
public class MultiPositionLabelLayer : LabelLayer
{
    private readonly PointF[] _positionOffsets = new[]
    {
        new PointF(0, -15),   // 上方
        new PointF(15, 0),    // 右侧
        new PointF(0, 15),    // 下方
        new PointF(-15, 0),   // 左侧
        new PointF(10, -10),  // 右上
        new PointF(10, 10),   // 右下
        new PointF(-10, -10), // 左上
        new PointF(-10, 10)   // 左下
    };
    
    // 实现多位置尝试的渲染逻辑
    // ...
}

8.4 标注旋转

8.4.1 使用属性字段旋转

// 使用角度字段
labelLayer.RotationColumn = "ANGLE";

// 使用方位角字段
labelLayer.RotationColumn = "BEARING";

8.4.2 动态计算旋转角度

// 根据线的方向旋转
labelLayer.GetRotationDelegate = (row) =>
{
    if (row.Geometry is LineString line && line.NumPoints >= 2)
    {
        // 获取线的起点和终点
        var start = line.GetPointN(0).Coordinate;
        var end = line.GetPointN(line.NumPoints - 1).Coordinate;
        
        // 计算角度
        double angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
        double degrees = angle * 180 / Math.PI;
        
        // 调整角度确保文字不会上下颠倒
        if (degrees > 90 || degrees < -90)
        {
            degrees += 180;
        }
        
        return (float)degrees;
    }
    return 0;
};

8.4.3 沿线标注

// 道路标注配置
var roadLabelLayer = new LabelLayer("Road Labels");
roadLabelLayer.DataSource = roadLayer.DataSource;
roadLabelLayer.LabelColumn = "ROAD_NAME";

roadLabelLayer.Style = new LabelStyle
{
    Font = new Font("Arial", 9),
    ForeColor = Color.FromArgb(100, 100, 100),
    Halo = new Pen(Color.FromArgb(230, 255, 255, 255), 2),
    HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center,
    VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Middle,
    CollisionDetection = true,
    CollisionBuffer = new SizeF(20, 5)
};

// 沿线显示
roadLabelLayer.GetRotationDelegate = (row) =>
{
    if (row.Geometry is LineString line && line.Length > 0)
    {
        // 计算线中点处的方向
        var midPoint = line.GetPointN(line.NumPoints / 2);
        var nextPoint = line.GetPointN(Math.Min(line.NumPoints / 2 + 1, line.NumPoints - 1));
        
        double dx = nextPoint.X - midPoint.X;
        double dy = nextPoint.Y - midPoint.Y;
        double angle = Math.Atan2(dy, dx) * 180 / Math.PI;
        
        // 确保文字不会上下颠倒
        if (angle > 90) angle -= 180;
        if (angle < -90) angle += 180;
        
        return (float)angle;
    }
    return 0;
};

8.5 多部分几何的标注

8.5.1 标注行为设置

// 标注所有部分
labelLayer.MultipartGeometryBehaviour = 
    LabelLayer.MultipartGeometryBehaviourEnum.All;

// 只标注第一个部分
labelLayer.MultipartGeometryBehaviour = 
    LabelLayer.MultipartGeometryBehaviourEnum.First;

// 标注最大的部分
labelLayer.MultipartGeometryBehaviour = 
    LabelLayer.MultipartGeometryBehaviourEnum.Largest;

// 标注公共中心
labelLayer.MultipartGeometryBehaviour = 
    LabelLayer.MultipartGeometryBehaviourEnum.CommonCenter;

8.5.2 多边形标注位置

// 使用质心
// 默认行为:使用几何的质心作为标注位置

// 自定义标注位置
labelLayer.GetLabelPointDelegate = (row) =>
{
    var geometry = row.Geometry;
    
    if (geometry is Polygon polygon)
    {
        // 使用内点(确保点在多边形内部)
        return polygon.InteriorPoint;
    }
    
    // 默认使用质心
    return geometry.Centroid;
};

8.6 标注过滤

8.6.1 比例尺过滤

// 设置可见比例尺范围
labelLayer.MinVisible = 1000;     // 最小可见比例尺
labelLayer.MaxVisible = 500000;   // 最大可见比例尺

// 大比例尺显示详细标注
var detailLabelLayer = new LabelLayer("Detail Labels");
detailLabelLayer.MinVisible = 0;
detailLabelLayer.MaxVisible = 10000;

// 小比例尺显示概要标注
var summaryLabelLayer = new LabelLayer("Summary Labels");
summaryLabelLayer.MinVisible = 10000;
summaryLabelLayer.MaxVisible = 1000000;

8.6.2 属性过滤

// 使用定义查询过滤
if (labelLayer.DataSource is ShapeFile shp)
{
    // ShapeFile 不直接支持定义查询
    // 需要使用其他方式
}

// 使用标注委托过滤
labelLayer.LabelStringDelegate = (row) =>
{
    // 只标注人口超过 10 万的城市
    double population = Convert.ToDouble(row["POPULATION"] ?? 0);
    if (population < 100000)
        return null;  // 返回 null 不显示标注
    
    return row["NAME"]?.ToString();
};

8.6.3 长度过滤

// 设置线的最小长度(用于道路标注)
labelLayer.Style.IgnoreLength = false;  // 启用长度检查

// 自定义长度检查
labelLayer.LabelStringDelegate = (row) =>
{
    if (row.Geometry is LineString line)
    {
        // 线太短时不显示标注
        if (line.Length < 0.01)  // 根据坐标系调整
            return null;
    }
    
    return row["NAME"]?.ToString();
};

8.7 标注主题

8.7.1 基于属性的标注样式

public class LabelTheme : ITheme
{
    private readonly string _typeColumn;
    private readonly Dictionary<string, LabelStyle> _styles;
    
    public LabelTheme(string typeColumn)
    {
        _typeColumn = typeColumn;
        _styles = new Dictionary<string, LabelStyle>();
    }
    
    public void AddStyle(string type, LabelStyle style)
    {
        _styles[type] = style;
    }
    
    public IStyle GetStyle(FeatureDataRow row)
    {
        string type = row[_typeColumn]?.ToString();
        
        if (!string.IsNullOrEmpty(type) && _styles.ContainsKey(type))
        {
            return _styles[type];
        }
        
        return new LabelStyle
        {
            Font = new Font("Arial", 10),
            ForeColor = Color.Black
        };
    }
}

// 使用
var labelTheme = new LabelTheme("CITY_TYPE");

labelTheme.AddStyle("Capital", new LabelStyle
{
    Font = new Font("Arial", 14, FontStyle.Bold),
    ForeColor = Color.Red,
    Halo = new Pen(Color.White, 3)
});

labelTheme.AddStyle("Provincial", new LabelStyle
{
    Font = new Font("Arial", 12, FontStyle.Bold),
    ForeColor = Color.DarkBlue,
    Halo = new Pen(Color.White, 2)
});

labelTheme.AddStyle("City", new LabelStyle
{
    Font = new Font("Arial", 10),
    ForeColor = Color.Black,
    Halo = new Pen(Color.White, 1)
});

labelLayer.Theme = labelTheme;

8.7.2 基于数值的动态字号

public class DynamicSizeLabelTheme : ITheme
{
    private readonly string _sizeColumn;
    private readonly double _minValue;
    private readonly double _maxValue;
    private readonly float _minSize;
    private readonly float _maxSize;
    
    public DynamicSizeLabelTheme(string sizeColumn, double minValue, double maxValue, 
        float minSize, float maxSize)
    {
        _sizeColumn = sizeColumn;
        _minValue = minValue;
        _maxValue = maxValue;
        _minSize = minSize;
        _maxSize = maxSize;
    }
    
    public IStyle GetStyle(FeatureDataRow row)
    {
        double value = Convert.ToDouble(row[_sizeColumn] ?? _minValue);
        
        // 计算字号
        double ratio = (value - _minValue) / (_maxValue - _minValue);
        ratio = Math.Max(0, Math.Min(1, ratio));
        float fontSize = _minSize + (float)((_maxSize - _minSize) * ratio);
        
        return new LabelStyle
        {
            Font = new Font("Arial", fontSize, FontStyle.Bold),
            ForeColor = Color.Black,
            Halo = new Pen(Color.White, Math.Max(1, fontSize / 6)),
            HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center,
            VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Middle,
            CollisionDetection = true
        };
    }
}

// 使用:根据人口大小调整字号
labelLayer.Theme = new DynamicSizeLabelTheme("POPULATION", 
    100000,    // 最小人口
    10000000,  // 最大人口
    8,         // 最小字号
    18);       // 最大字号

8.8 性能优化

8.8.1 减少标注数量

// 1. 使用比例尺过滤
labelLayer.MinVisible = 5000;
labelLayer.MaxVisible = 100000;

// 2. 使用优先级减少标注
labelLayer.PriorityColumn = "IMPORTANCE";
labelLayer.Style.CollisionDetection = true;

// 3. 使用属性过滤
labelLayer.LabelStringDelegate = (row) =>
{
    int importance = Convert.ToInt32(row["IMPORTANCE"] ?? 0);
    if (importance < 5)
        return null;
    return row["NAME"]?.ToString();
};

8.8.2 简化碰撞检测

// 使用较小的碰撞缓冲区
labelLayer.Style.CollisionBuffer = new SizeF(2, 2);

// 禁用碰撞检测(谨慎使用)
labelLayer.Style.CollisionDetection = false;

8.8.3 缓存标注结果

public class CachedLabelLayer : LabelLayer
{
    private Image _cachedLabels;
    private Envelope _cachedEnvelope;
    private double _cachedZoom;
    
    public override void Render(Graphics g, Map map)
    {
        // 检查缓存是否有效
        if (_cachedLabels != null && 
            _cachedEnvelope.Equals(map.Envelope) && 
            Math.Abs(_cachedZoom - map.Zoom) < 0.001)
        {
            // 使用缓存
            g.DrawImage(_cachedLabels, 0, 0);
            return;
        }
        
        // 创建新的缓存
        _cachedLabels = new Bitmap(map.Size.Width, map.Size.Height);
        using (var tempG = Graphics.FromImage(_cachedLabels))
        {
            tempG.Clear(Color.Transparent);
            base.Render(tempG, map);
        }
        
        _cachedEnvelope = map.Envelope;
        _cachedZoom = map.Zoom;
        
        g.DrawImage(_cachedLabels, 0, 0);
    }
    
    public void InvalidateCache()
    {
        _cachedLabels?.Dispose();
        _cachedLabels = null;
    }
}

8.9 本章小结

本章详细介绍了 SharpMap 的标注图层系统:

  1. LabelLayer 基础:了解了标注图层的创建和配置
  2. 标注样式:掌握了字体、颜色、对齐等样式设置
  3. 碰撞检测:学习了避免标注重叠的配置方法
  4. 标注旋转:了解了沿线标注和动态旋转
  5. 多部分几何:掌握了复杂几何的标注处理
  6. 标注过滤:学习了基于比例尺和属性的过滤
  7. 标注主题:了解了动态标注样式的实现
  8. 性能优化:掌握了标注性能优化技巧

8.10 参考资源


下一章预告:第09章将详细介绍瓦片图层与在线地图的集成,包括 BruTile、WMS 等服务的使用。

posted @ 2026-01-08 14:09  我才是银古  阅读(13)  评论(0)    收藏  举报