10

I'm trying to create dynamic leader lines by using a PostGIS view in addition to the QGIS „Move Label“ tool.

CREATE VIEW leader_line AS
SELECT
gid,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(xcord_label, ycord_label), SRID))::geometry(linestring, SRID) AS geom
FROM point
WHERE xcord_label IS NOT NULL;

This works fine for all labels WHERE ST_X(geom) < xcord_label but creates wrong looking leader lines for labels WHERE ST_X(geom) > xcord_label.

enter image description here enter image description here

Does anybody know how to get properly placed leader lines for labels WHERE ST_X(geom) > xcord_label? Is there any way to refer to the xmax coordinate of the labels?

enter image description here

  • 1
    are your labels in points or map units?If it's map units it should be fairly easy to guess the height, and thereby shorten your leader line to compensate) – Steven Kay Jul 16 '15 at 19:47
  • The label size is in map units. – Lunar Sea Jul 16 '15 at 20:14
9
+50

You can use QGIS' quadrant placement specifier determined from the line's azimuth to place a better label. The quadrant specifies 8 positions around a point:

[ 0=Above Left | 1=Above | 2=Above Right |
  3=Left       | 4=Over  | 5=Right       |
  6=Below Left | 7=Below | 8=Below Right ]

Here's an example around Null Island, creating a table and two views.

CREATE TABLE points (
  gid serial PRIMARY KEY,
  geom geometry(Point, 4326),
  label_geom geometry(Point, 4326),
  label text
);

INSERT INTO points(geom, label_geom, label)
SELECT origin, pt, round(degrees(ST_Azimuth(origin, pt))) || ' degrees'
FROM (
  SELECT
    ST_SetSRID(ST_MakePoint(0, 0), 4326) AS origin,
    ST_SetSRID(ST_MakePoint(cos(radians(x)), sin(radians(x))), 4326) AS pt
  FROM generate_series(0, 350, 15) AS x
) AS f;

CREATE OR REPLACE VIEW point_labels AS
  SELECT gid, label_geom AS geom,
  CASE
    WHEN ST_Azimuth(geom, label_geom) ISNULL THEN 2 -- default if azimuth cannot be determined
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 22.5 THEN 1 -- Above
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 67.5 THEN 2 -- Above Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 112.5 THEN 5 -- Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 157.5 THEN 8 -- Below Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 202.5 THEN 7 -- Below
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 247.5 THEN 6 -- Below Left
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 292.5 THEN 3 -- Left
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 337.5 THEN 0 -- Above Left
    ELSE 1 -- >= 337.5 Above
  END AS quadrant, label
  FROM points;

CREATE OR REPLACE VIEW leader_line AS
  SELECT gid, ST_MakeLine(geom, label_geom)::geometry(LineString, 4326) AS geom, label
  FROM points;

Then in QGIS, add:

  • pointsgeom
  • leader_linegeom – primary key needs to be gid
  • point_labelsgeom – primary key needs to be gid

QGIS

Now configure the layer properties for point_labels:

  • Change style so the point is not drawn, e.g., change size to 0.0
  • Label this layer with label, and change placement to "Offset from point", modifying the "Quadrant" to use the attribute field quadrant

quadrant

Bingo!

Bingo

Note that a slightly different approach is required for geography types, since ST_Azimuth behaves differently.


Update: When adding new points to the points layer, the geom field is updated as usual, but the label_geom is not. To populate a default value of label_geom with new points, a trigger needs to be created. But if a trigger function is used, the quadrant specifier can be stored in the points table and the point_labels view can be ignored:

For example, let's start again with a slightly different example with one table and one view:

-- DROP TABLE points CASCADE;
CREATE TABLE points (
  gid serial PRIMARY KEY,
  geom geometry(Point, 4326),
  label_geom geometry(Point, 4326),
  quadrant integer,
  label text
);

CREATE FUNCTION label_geom_tg_fn() RETURNS trigger AS
$BODY$
DECLARE
  azimuth float8;
BEGIN
  -- Set a default label_geom
  IF NEW.label_geom ISNULL THEN
    NEW.label_geom := NEW.geom;
  END IF;
  -- Determine quadrant
  azimuth := degrees(ST_Azimuth(NEW.geom, NEW.label_geom));
  NEW.quadrant := CASE
    WHEN azimuth ISNULL THEN 2 -- azimuth cannot be determined, so put Above Right
    WHEN azimuth < 22.5 THEN 1 -- Above
    WHEN azimuth < 67.5 THEN 2 -- Above Right
    WHEN azimuth < 112.5 THEN 5 -- Right
    WHEN azimuth < 157.5 THEN 8 -- Below Right
    WHEN azimuth < 202.5 THEN 7 -- Below
    WHEN azimuth < 247.5 THEN 6 -- Below Left
    WHEN azimuth < 292.5 THEN 3 -- Left
    WHEN azimuth < 337.5 THEN 0 -- Above Left
    ELSE 1 END;-- >= 337.5 Above
  RETURN NEW;
END;$BODY$ LANGUAGE plpgsql;

CREATE TRIGGER label_geom_tg BEFORE INSERT OR UPDATE
   ON points FOR EACH ROW
   EXECUTE PROCEDURE label_geom_tg_fn();

The from the first example, re-do the INSERT INTO points and CREATE OR REPLACE VIEW leader_line statements, as these do not require modification. But ignore the leader_line view.

Then in QGIS, add:

  • pointsgeom
  • pointslabel_geom
  • leader_linegeom – primary key needs to be gid

Now configure the layer properties for points with label_geom as the first example did for point_labels. The quadrant specifier will be modified automatically for new and moved points, but you will only notice these changes each time you save your edits.

  • Great work, but how to add a new point feature in QGIS having two geometry columns in one PostGIS table? – Lunar Sea Jul 18 '15 at 18:04
  • @Lunar Sea - interesting, do you get two entries for the table, one per geometry, but qgis doesn't let you set the geometry field from the combo? Have you tried using a manual sql query in the import dialog box (it's the rightmost column, and often hidden out of view...)? – Steven Kay Jul 18 '15 at 19:41
  • I've two 'points' layers in QGIS (gid | label_geom | label and gid, geom, label). – Lunar Sea Jul 18 '15 at 19:57
  • @LunarSea I reworked a second example that has one table and one view. The table has trigger functions to determine a default value for the label_geom, and also updates the quadrant value too, so the point_label layer/view is no longer needed. – Mike T Jul 19 '15 at 0:53
  • Nice workaround Mike! After moving a label_geom I've to save the layer edit and refresh the canvas to see the actual position of the label. It's a pity that there is no way using a quadrant specifier with the QGIS "Move label" tool. – Lunar Sea Jul 19 '15 at 15:37
1

okay.. as it's in map units this should be fairly straight-forward, within limitations. You already know the height of the label. If it was in points it would be scale-dependant.

This assumes a fixed label size, so how well this works depends on how uniform your labels are, and whether or not you use a proportional or fixed-width font (fixed width is easier - multiply the length of the label by the label size to get the label width).

Sadly this doesn't answer your question on how to actually find the bounds of the label as rendered.

you have 4 cases (NE,NW,SE,SW).

i assume your table looks like this (apologies, some field names are different)

CREATE TABLE points
(
  uniq int PRIMARY KEY,
  geom geometry(Point,27700),
  label_x int,
  label_y int,
  labeltext character varying(100)
);
ALTER TABLE points
  OWNER TO user;
GRANT ALL ON TABLE points TO user;
GRANT SELECT ON TABLE points TO public;

Next, add 4 points (all identical) but with labels in the 4 quadrants to represent the 4 main use cases

insert into points values 
(1,ST_SetSRID(ST_Point(1000,1000),27700),750,750,'123');

insert into points values(2,ST_SetSRID(ST_Point(1000,1000),27700),1250,1250,'456')

insert into points values 
(3,ST_SetSRID(ST_Point(1000,1000),27700),750,1250,'456')

insert into points values 
(4,ST_SetSRID(ST_Point(1000,1000),27700),1250,750,'789')

I used CRS 27700 (0,0 in lower-left, map units in m) I've assumed a label width 50, height 30 map units.

-- SW use case
CREATE OR REPLACE VIEW leader_line_sw AS
SELECT
uniq,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(label_x+50, label_y+30), 27700))::geometry(linestring, 27700) AS geom
FROM points
WHERE label_x IS NOT NULL AND 
label_y<=ST_Y(geom) and label_x<=ST_X(geom);

-- SE use case
CREATE OR REPLACE VIEW leader_line_se AS
SELECT
uniq,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(label_x, label_y-30), 27700))::geometry(linestring, 27700) AS geom
FROM points
WHERE label_x IS NOT NULL AND 
label_y<=ST_Y(geom) and label_x>ST_X(geom);


-- NE use case
CREATE OR REPLACE VIEW leader_line_ne AS
SELECT
uniq,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(label_x, label_y), 27700))::geometry(linestring, 27700) AS geom
FROM points
WHERE label_x IS NOT NULL AND 
label_y>ST_Y(geom) and label_x>ST_X(geom);

-- NW use case
CREATE OR REPLACE VIEW leader_line_nw2 AS
SELECT
uniq,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(label_x+50, label_y), 27700))::geometry(linestring, 27700) AS geom
FROM points
WHERE label_x IS NOT NULL AND 
label_y>ST_Y(geom) and label_x<=ST_X(geom);

Affine Transformations

Another possibility is to shorten all leading lines, to say 80%.

  • You could use ST_Translate(geom,-ST_X(geom),-ST_Y(geom)) to move the line to the origin to get geom_o
  • use ST_Scale(geom_o,0.8,0.8) to get geom_o_scaled
  • then retranslate using ST_Translate(geom_o_scaled,ST_X(geom),ST_Y(geom)) back to the original position.

This might work better, although I haven't tried it.

  • Thank you for your effords, unfortunately the leader lines don't match the labels very well. – Lunar Sea Jul 18 '15 at 19:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.