Esempio n. 1
0
class District(db.Model):
    """ NPTG district. """
    __tablename__ = "district"

    code = db.Column(db.VARCHAR(3), primary_key=True)
    name = db.Column(db.Text, nullable=False, index=True)
    admin_area_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("admin_area.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    modified = db.deferred(db.Column(db.DateTime))

    localities = db.relationship("Locality", backref="district",
                                 order_by="Locality.name", lazy="raise")
    postcodes = db.relationship("Postcode", backref="district",
                                order_by="Postcode.text", lazy="raise")

    def __repr__(self):
        return f"<District({self.code!r})>"

    def list_localities(self):
        """ Queries all localities containing active stops. """
        query_local = (
            Locality.query
            .filter(Locality.district_ref == self.code,
                    Locality.stop_points.any(StopPoint.active))
            .order_by(Locality.name)
        )

        return query_local.all()
Esempio n. 2
0
class Postcode(db.Model):
    """ Postcodes with coordinates, derived from the NSPL data. """
    __tablename__ = "postcode"

    index = db.Column(db.VARCHAR(7), primary_key=True)
    text = db.Column(db.VARCHAR(8), index=True, unique=True, nullable=False)
    admin_area_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("admin_area.code", ondelete="CASCADE"),
        index=True
    )
    district_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("district.code", ondelete="CASCADE"),
        index=True
    )
    latitude = db.Column(db.Float, nullable=False)
    longitude = db.Column(db.Float, nullable=False)
    easting = db.deferred(db.Column(db.Integer, nullable=False))
    northing = db.deferred(db.Column(db.Integer, nullable=False))

    def __repr__(self):
        return f"<Postcode({self.text!r})>"

    def stops_in_range(self, *options):
        """ Returns a list of all stop points within range.

            :param options: Options for loading model instances, eg load_only
            :returns: List of StopPoint objects with distance attribute added
            and sorted.
        """
        return StopPoint.in_range(self.latitude, self.longitude, *options)
Esempio n. 3
0
class JourneyLink(db.Model):
    """ Stop with timing and journey info..

        Each stop has the following fields:
        - ATCO code for stop as foreign key
        - Whether this is a timing info point (expected to be timetabled)
        - Whether this is a principal point (services must stop here)
        - Whether the bus stops or passes by
    """
    __tablename__ = "journey_link"

    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    pattern_ref = db.Column(
        db.Integer,
        db.ForeignKey("journey_pattern.id", ondelete="CASCADE"),
        nullable=False, index=True
    )
    stop_point_ref = db.Column(
        db.VARCHAR(12),
        db.ForeignKey("stop_point.atco_code", ondelete="CASCADE"),
        nullable=True, index=True
    )
    run_time = db.Column(db.Interval, nullable=True)
    wait_arrive = db.Column(db.Interval, nullable=True)
    wait_leave = db.Column(db.Interval, nullable=True)
    timing_point = db.Column(db.Boolean, nullable=False)
    principal_point = db.Column(db.Boolean, nullable=False)
    stopping = db.Column(db.Boolean, nullable=False)
    sequence = db.Column(db.Integer, index=True)

    __table_args__ = (
        db.UniqueConstraint("pattern_ref", "sequence"),
        db.CheckConstraint("run_time IS NOT NULL AND wait_arrive IS NOT NULL "
                           "OR wait_leave IS NOT NULL")
    )
Esempio n. 4
0
class AdminArea(db.Model):
    """ NPTG administrative area. """
    __tablename__ = "admin_area"

    code = db.Column(db.VARCHAR(3), primary_key=True)
    name = db.Column(db.Text, nullable=False, index=True)
    atco_code = db.deferred(db.Column(db.VARCHAR(3), unique=True, nullable=False))
    region_ref = db.Column(
        db.VARCHAR(2),
        db.ForeignKey("region.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    is_live = db.deferred(db.Column(db.Boolean, default=True))
    modified = db.deferred(db.Column(db.DateTime))

    districts = db.relationship("District", backref="admin_area",
                                order_by="District.name", lazy="raise")
    localities = db.relationship("Locality", backref="admin_area",
                                 innerjoin=True, order_by="Locality.name",
                                 lazy="raise")
    postcodes = db.relationship("Postcode", backref="admin_area",
                                innerjoin=True, order_by="Postcode.text",
                                lazy="raise")
    stop_points = db.relationship(
        "StopPoint", backref="admin_area", innerjoin=True,
        order_by="StopPoint.name, StopPoint.short_ind", lazy="raise"
    )
    stop_areas = db.relationship("StopArea", backref="admin_area",
                                 innerjoin=True, order_by="StopArea.name",
                                 lazy="raise")

    def __repr__(self):
        return f"<AdminArea({self.code!r})>"

    def list_localities(self):
        """ Queries all localities containing active stops. """
        query_local = (
            Locality.query
            .filter(Locality.admin_area_ref == self.code,
                    Locality.stop_points.any(StopPoint.active))
            .order_by(Locality.name)
        )

        return query_local.all()
Esempio n. 5
0
class StopArea(db.Model):
    """ NaPTAN stop areas, eg bus interchanges. """
    __tablename__ = "stop_area"

    code = db.Column(db.VARCHAR(12), primary_key=True)
    name = db.Column(db.Text, nullable=False, index=True)
    admin_area_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("admin_area.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    locality_ref = db.Column(
        db.VARCHAR(8),
        db.ForeignKey("locality.code", ondelete="CASCADE"),
        index=True
    )
    stop_area_type = db.Column(db.VARCHAR(4), nullable=False)
    active = db.Column(db.Boolean, nullable=False, index=True)
    latitude = db.Column(db.Float, nullable=False)
    longitude = db.Column(db.Float, nullable=False)
    easting = db.deferred(db.Column(db.Integer, nullable=False))
    northing = db.deferred(db.Column(db.Integer, nullable=False))
    modified = db.deferred(db.Column(db.DateTime))

    # Number of stop points associated with this stop area
    stop_count = db.deferred(
        db.select([db.cast(db.func.count(), db.Text)])
        .where((_stop_point.c.stop_area_ref == code) & _stop_point.c.active)
        .scalar_subquery()
    )

    stop_points = db.relationship(
        "StopPoint", backref="stop_area",
        order_by="StopPoint.name, StopPoint.short_ind", lazy="raise"
    )

    def __repr__(self):
        return f"<StopArea({self.code!r})>"
Esempio n. 6
0
class Region(db.Model):
    """ NPTG region. """
    __tablename__ = "region"

    code = db.Column(db.VARCHAR(2), primary_key=True)
    name = db.Column(db.Text, nullable=False, index=True)
    modified = db.deferred(db.Column(db.DateTime))

    areas = db.relationship("AdminArea", backref="region", innerjoin=True,
                            order_by="AdminArea.name", lazy="raise")
    patterns = db.relationship("JourneyPattern", backref="region",
                               innerjoin=True, lazy="raise")

    def __repr__(self):
        return f"<Region({self.code!r})>"
Esempio n. 7
0
class Operator(db.Model):
    """ Bus/metro service operator. """
    __tablename__ = "operator"

    SPLIT_ADDRESS = re.compile(r"\s*,\s*")

    code = db.Column(db.Text, primary_key=True)
    region_ref = db.Column(
        db.VARCHAR(2),
        db.ForeignKey("region.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    name = db.Column(db.Text, nullable=False)
    mode = db.Column(
        db.Integer,
        db.ForeignKey("service_mode.id"),
        nullable=False, index=True
    )
    licence_name = db.deferred(db.Column(db.Text, nullable=True))
    email = db.deferred(db.Column(db.Text), group="contacts")
    address = db.deferred(db.Column(db.Text), group="contacts")
    website = db.deferred(db.Column(db.Text), group="contacts")
    twitter = db.deferred(db.Column(db.Text), group="contacts")

    local_codes = db.relationship(
        "LocalOperator",
        backref=db.backref("operator", innerjoin=True, uselist=False),
        order_by="LocalOperator.code",
        lazy="raise"
    )
    patterns = db.relationship(
        "JourneyPattern",
        backref=db.backref("operator", innerjoin=True, viewonly=True,
                           uselist=False),
        secondary="local_operator",
        viewonly=True,
        lazy="raise"
    )

    @property
    def split_address(self):
        if "address" not in db.inspect(self).unloaded:
            return self.SPLIT_ADDRESS.split(self.address)
        else:
            return None
Esempio n. 8
0
class JourneyPattern(db.Model):
    """ Sequences of timing links. """
    __tablename__ = "journey_pattern"

    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    origin = db.Column(db.Text)
    destination = db.Column(db.Text)

    service_ref = db.Column(
        db.Integer,
        db.ForeignKey("service.id", ondelete="CASCADE"),
        nullable=False, index=True
    )
    local_operator_ref = db.Column(db.Text, nullable=False, index=True)
    region_ref = db.Column(
        db.VARCHAR(2),
        db.ForeignKey("region.code", ondelete="CASCADE"),
        nullable=False, index=True
    )

    direction = db.Column(db.Boolean, nullable=False, index=True)
    date_start = db.Column(db.Date, nullable=False)
    date_end = db.Column(db.Date)

    __table_args__ = (
        db.CheckConstraint("date_start <= date_end"),
        db.ForeignKeyConstraint(
            ["local_operator_ref", "region_ref"],
            ["local_operator.code", "local_operator.region_ref"],
            ondelete="CASCADE"
        ),
        db.Index("ix_journey_pattern_local_operator_ref_region_ref",
                 "local_operator_ref", "region_ref")
    )

    links = db.relationship("JourneyLink", backref="pattern", innerjoin=True,
                            order_by="JourneyLink.sequence", lazy="raise")
    journeys = db.relationship("Journey", backref="pattern", innerjoin=True,
                               lazy="raise")
Esempio n. 9
0
class LocalOperator(db.Model):
    """ Operator codes within regions for each operator. """
    __tablename__ = "local_operator"

    code = db.Column(db.Text, primary_key=True)
    region_ref = db.Column(
        db.VARCHAR(2),
        db.ForeignKey("region.code", ondelete="CASCADE"),
        primary_key=True, index=True
    )
    operator_ref = db.Column(
        db.Text,
        db.ForeignKey("operator.code", ondelete="CASCADE"),
        index=True
    )
    name = db.Column(db.Text, nullable=True)

    patterns = db.relationship(
        "JourneyPattern",
        backref=db.backref("local_operator", innerjoin=True, uselist=False,
                           viewonly=True),
        viewonly=True,
        lazy="raise"
    )
Esempio n. 10
0
class FTS(db.Model):
    """ Materialized view for full text searching.

        All rankings are done with weights 0.125, 0.25, 0.5 and 1.0 for ``D``
        to ``A`` respectively.
    """
    __tablename__ = "fts"

    table_name = db.Column(db.Text, index=True, primary_key=True)
    code = db.Column(db.Text, index=True, primary_key=True)
    name = db.Column(db.Text, nullable=False)
    indicator = db.Column(db.Text(collation="utf8_numeric"))
    street = db.Column(db.Text)
    stop_type = db.Column(db.Text)
    stop_area_ref = db.Column(db.Text)
    locality_name = db.Column(db.Text)
    district_name = db.Column(db.Text)
    admin_area_ref = db.Column(db.VARCHAR(3))
    admin_area_name = db.Column(db.Text)
    admin_areas = db.Column(pg.ARRAY(db.Text, dimensions=1), nullable=False)
    vector = db.Column(pg.TSVECTOR, nullable=False)

    # Unique index for table_name + code required for concurrent refresh
    __table_args__ = (db.Index("ix_fts_unique",
                               "table_name",
                               "code",
                               unique=True),
                      db.Index("ix_fts_vector_gin",
                               "vector",
                               postgresql_using="gin"),
                      db.Index("ix_fts_areas_gin",
                               "admin_areas",
                               postgresql_using="gin"))

    GROUP_NAMES = {
        "area": "Areas",
        "place": "Places",
        "stop": "Stops",
        "service": "Services"
    }
    GROUPS = {
        "area": {"region", "admin_area", "district"},
        "place": {"locality"},
        "stop": {"stop_area", "stop_point"},
        "service": {"service"}
    }

    DICTIONARY = "english"
    WEIGHTS = "{0.125, 0.25, 0.5, 1.0}"

    def __repr__(self):
        return f"<FTS({self.table_name!r}, {self.code!r})>"

    @classmethod
    def match(cls, query):
        """ Full text search expression with a tsquery.

            :param query: Query as string.
            :returns: Expression to be used in a query.
        """
        dict_ = db.bindparam("dictionary", cls.DICTIONARY)
        return cls.vector.op("@@")(db.func.websearch_to_tsquery(dict_, query))

    @classmethod
    def ts_rank(cls, query):
        """ Full text search rank expression with a tsquery.

            :param query: Query as string.
            :returns: Expression to be used in a query.
        """
        dict_ = db.bindparam("dictionary", cls.DICTIONARY)
        tsquery = db.func.querytree(db.func.websearch_to_tsquery(dict_, query))

        return db.func.ts_rank(db.bindparam("weights", cls.WEIGHTS),
                               cls.vector, db.cast(tsquery, TSQUERY), 1)

    @classmethod
    def _apply_filters(cls, match, groups=None, areas=None):
        """ Apply filters to a search expression if they are specified.

            :param match: The original query expression
            :param groups: Groups, eg 'stop' or 'area'
            :param areas: Administrative area codes to filter by
            :returns: Query expression with added filters, if any
        """
        if groups is not None:
            if set(groups) - cls.GROUP_NAMES.keys():
                raise ValueError(f"Groups {groups!r} contain invalid values.")
            tables = []
            for g in groups:
                tables.extend(cls.GROUPS[g])
            match = match.filter(cls.table_name.in_(tables))

        if areas is not None:
            match = match.filter(cls.admin_areas.overlap(areas))

        return match

    @classmethod
    def search(cls, query, groups=None, admin_areas=None):
        """ Creates an expression for searching queries, excluding stop points
            within areas that already match.

            :param query: web search query as string.
            :param groups: Set with values 'area', 'place' or 'stop'.
            :param admin_areas: List of administrative area codes to filter by.
            :returns: Query expression to be executed.
        """
        if not _test_query(query):
            return None

        fts_sa = db.aliased(cls)
        rank = cls.ts_rank(query)
        # Defer vector and admin areas, and order by rank, name and indicator
        match = (
            cls.query.options(db.defer(cls.vector), db.defer(
                cls.admin_areas)).filter(
                    cls.match(query),
                    # Ignore stops whose stop areas already match
                    ~db.exists().where(
                        fts_sa.match(query)
                        & (fts_sa.code == cls.stop_area_ref))).order_by(
                            db.desc(rank), cls.name, cls.indicator))
        # Add filters for groups or admin area
        match = cls._apply_filters(match, groups, admin_areas)

        return match

    @classmethod
    def matching_groups(cls, query, admin_areas=None):
        """ Finds all admin areas and table names covering matching results for
            a query.

            The areas and groups are sorted into two sets, to be used for
            filtering.

            :param query: web search query as string.
            :param admin_areas: Filter by admin areas to get matching groups
            :returns: A tuple with a dict of groups and a dict with
            administrative area references and names
        """
        if not _test_query(query):
            return None

        array_t = pg.array_agg(db.distinct(cls.table_name))
        if admin_areas is not None:
            # Filter array of tables by whether aggregated rows' admin areas
            # match those already selected
            array_t = array_t.filter(cls.admin_areas.overlap(admin_areas))

        array_areas = pg.array((AdminArea.code, AdminArea.name))
        array_a = pg.array_agg(db.distinct(array_areas))

        result = (db.session.query(
            array_t.label("tables"), array_a.label("areas")).select_from(
                db.func.unnest(cls.admin_areas).alias("unnest_areas")).join(
                    AdminArea,
                    db.column("unnest_areas") == AdminArea.code).filter(
                        cls.match(query)).one())

        # All data should have been aggregated into one row
        tables = set(result.tables) if result.tables is not None else set()
        groups = {
            g: n
            for g, n in cls.GROUP_NAMES.items() if tables & cls.GROUPS[g]
        }
        areas = dict(result.areas) if result.areas is not None else {}

        return groups, areas
Esempio n. 11
0
class StopPoint(db.Model):
    """ NaPTAN stop points, eg bus stops. """
    __tablename__ = "stop_point"

    atco_code = db.Column(db.VARCHAR(12), primary_key=True)
    naptan_code = db.Column(db.VARCHAR(9), index=True, unique=True,
                            nullable=False)
    name = db.Column(db.Text, nullable=False, index=True)
    landmark = db.Column(db.Text)
    street = db.Column(db.Text)
    crossing = db.Column(db.Text)
    indicator = db.Column(db.Text, default="", nullable=False)
    short_ind = db.Column(
        db.Text(collation="utf8_numeric"),
        index=True,
        default="",
        nullable=False
    )
    locality_ref = db.Column(
        db.VARCHAR(8),
        db.ForeignKey("locality.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    admin_area_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("admin_area.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    stop_area_ref = db.Column(
        db.VARCHAR(12),
        db.ForeignKey("stop_area.code", ondelete="CASCADE"),
        index=True
    )
    stop_type = db.Column(db.VARCHAR(3), nullable=False)
    active = db.Column(db.Boolean, nullable=False, index=True)
    bearing = db.Column(db.VARCHAR(2))
    latitude = db.Column(db.Float, nullable=False, index=True)
    longitude = db.Column(db.Float, nullable=False, index=True)
    easting = db.deferred(db.Column(db.Integer, nullable=False))
    northing = db.deferred(db.Column(db.Integer, nullable=False))
    modified = db.deferred(db.Column(db.DateTime))

    # Distinct list of lines serving this stop
    lines = db.deferred(_array_lines(atco_code))

    locality = db.relationship("Locality", uselist=False,
                               back_populates="stop_points", lazy="raise")
    other_stops = db.relationship(
        "StopPoint",
        primaryjoin=(
            db.foreign(stop_area_ref).isnot(None) &
            (db.remote(stop_area_ref) == db.foreign(stop_area_ref)) &
            (db.remote(atco_code) != db.foreign(atco_code)) &
            db.remote(active)
        ),
        uselist=True,
        order_by="StopPoint.name, StopPoint.short_ind",
        lazy="raise"
    )
    links = db.relationship("JourneyLink", backref="stop_point", lazy="raise")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Declared in case it needs to be defined for stops near a point
        distance = None

    def __repr__(self):
        if "atco_code" in self.__dict__:
            repr_text = f"<StopPoint(atco_code={self.atco_code!r})>"
        else:
            repr_text = f"<StopPoint(naptan_code={self.naptan_code!r})>"

        return repr_text

    @property
    def long_name(self):
        if self.indicator:
            return f"{self.name} ({self.indicator})"
        else:
            return self.name

    @classmethod
    def from_list(cls, list_naptan_codes):
        """ Finds all stops from a list of NaPTAN codes, ordered using the
            same list.

            :param list_naptan_codes: List of NaPTAN/SMS codes.
            :returns: Ordered list of StopPoint objects.
        """

        if list_naptan_codes:
            def _stop_index(stop):
                return list_naptan_codes.index(stop.naptan_code)

            stops = (
                cls.query
                .options(db.joinedload(cls.locality, innerjoin=True))
                .filter(cls.naptan_code.in_(list_naptan_codes))
                .all()
            )
            stops.sort(key=_stop_index)
        else:
            stops = []

        return stops

    @classmethod
    def within_box(cls, box, *options, active_only=True):
        """ Finds all stop points within a box with latitude and longitude
            coordinates for each side.

            :param box: BoundingBox object with north, east, south and west
            attributes
            :param options: Options for loading model instances, eg load_only
            :param active_only: Active stops only
            :returns: Unordered list of StopPoint objects
        """
        query = cls.query
        if options:
            query = query.options(*options)
        if active_only:
            query = query.filter(cls.active)
        try:
            nearby_stops = query.filter(
                db.between(cls.latitude, box.south, box.north),
                db.between(cls.longitude, box.west, box.east)
            )
        except AttributeError:
            raise TypeError(f"Box {box!r} is not a valid BoundingBox object.")

        return nearby_stops.all()

    @classmethod
    def in_range(cls, latitude, longitude, *options, active_only=True):
        """ Finds stop points in range of lat/long coordinates.

            Returns an ordered list of stop points and their distances from
            said coordinates.

            :param latitude: Latitude of centre point
            :param longitude: Longitude of centre point
            :param options: Options for loading model instances, eg load_only
            :param active_only: Active stops only
            :returns: List of StopPoint objects with distance attribute added
            and sorted.
        """
        box = location.bounding_box(latitude, longitude, MAX_DIST)
        nearby_stops = cls.within_box(box, *options, active_only=active_only)

        stops = []
        for stop in nearby_stops:
            dist = location.get_distance((latitude, longitude),
                                         (stop.latitude, stop.longitude))
            if dist < MAX_DIST:
                stop.distance = dist
                stops.append(stop)

        return sorted(stops, key=lambda s: s.distance)

    def to_geojson(self):
        """ Outputs stop point data in GeoJSON format.

            :returns: JSON-serializable dict.
        """
        geojson = {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [self.longitude, self.latitude]
            },
            "properties": {
                "atcoCode": self.atco_code,
                "smsCode": self.naptan_code,
                "title": self.long_name,
                "name": self.name,
                "indicator": self.short_ind,
                "street": self.street,
                "bearing": self.bearing,
                "stopType": self.stop_type,
                "locality": self.locality.name,
                "adminAreaRef": self.admin_area_ref,
            }
        }

        return geojson

    def get_services(self):
        """ Queries and returns two datasets for services and operators at this
            stoplist including the origin and destination of these services,
            grouped by service ID and direction. Services are also checked for
            whether they terminate at this stop or not. Operators are returned
            as a dict of local operator codes and operator names.
        """
        # Checks if associated link is not last in sequence
        link = db.aliased(JourneyLink)
        next_link = (
            db.session.query(link.id)
            .filter(link.pattern_ref == JourneyLink.pattern_ref,
                    link.sequence == JourneyLink.sequence + 1)
            .as_scalar()
        )

        # Give service instance name in keyed tuple object
        service = db.aliased(Service, name="service")
        operator = pg.array((
            LocalOperator.code,
            db.func.coalesce(Operator.name, LocalOperator.name)
        ))
        query_services = (
            db.session.query(
                service,
                JourneyPattern.direction,
                db.func.string_agg(JourneyPattern.origin.distinct(), ' / ')
                .label("origin"),
                db.func.string_agg(JourneyPattern.destination.distinct(), ' / ')
                .label("destination"),
                (db.func.count(next_link) == 0).label("terminates"),
                pg.array_agg(db.distinct(operator)).label("operators")
            )
            .join(service.patterns)
            .join(JourneyPattern.links)
            .join(JourneyPattern.local_operator)
            .outerjoin(LocalOperator.operator)
            .filter(JourneyLink.stop_point_ref == self.atco_code)
            .group_by(service.id, JourneyPattern.direction)
            .order_by(service.line, service.description,
                      JourneyPattern.direction)
        )

        services = query_services.all()
        operators = {}
        for sv in services:
            operators.update(sv.operators)

        return services, operators

    def to_full_json(self):
        """ Produces full data for stop point in JSON format, including services
            and locality data.
        """
        services, operators = self.get_services()
        json = {
            "atcoCode": self.atco_code,
            "smsCode": self.naptan_code,
            "title": self.long_name,
            "name": self.name,
            "indicator": self.short_ind,
            "street": self.street,
            "crossing": self.crossing,
            "landmark": self.landmark,
            "bearing": self.bearing,
            "stopType": self.stop_type,
            "adminAreaRef": self.admin_area_ref,
            "latitude": self.latitude,
            "longitude": self.longitude,
            "active": self.active,
            "adminArea": {
                "code": self.admin_area.code,
                "name": self.admin_area.name,
            },
            "district": {
                "code": self.locality.district.code,
                "name": self.locality.district.name,
            } if self.locality.district is not None else None,
            "locality": {
                "code": self.locality.code,
                "name": self.locality.name,
            },
            "services": [{
                "code": s.service.code,
                "shortDescription": s.service.short_description,
                "line": s.service.line,
                "direction": "inbound" if s.direction else "outbound",
                "reverse": s.direction,
                "origin": s.origin,
                "destination": s.destination,
                "terminates": s.terminates,
                "operatorCodes": list(operators)
            } for s in services],
            "operators": [{
                "code": code, "name": name
            } for code, name in operators.items()]
        }

        return json
Esempio n. 12
0
class Locality(db.Model):
    """ NPTG locality. """
    __tablename__ = "locality"

    code = db.Column(db.VARCHAR(8), primary_key=True)
    name = db.Column(db.Text, nullable=False, index=True)
    parent_ref = db.deferred(db.Column(db.VARCHAR(8), index=True))
    admin_area_ref = db.Column(
        db.VARCHAR(3),
        db.ForeignKey("admin_area.code", ondelete="CASCADE"),
        nullable=False, index=True
    )
    district_ref = db.Column(db.VARCHAR(3),
                             db.ForeignKey("district.code", ondelete="CASCADE"),
                             index=True)
    latitude = db.deferred(db.Column(db.Float, nullable=False),
                           group="coordinates")
    longitude = db.deferred(db.Column(db.Float, nullable=False),
                            group="coordinates")
    easting = db.deferred(db.Column(db.Integer, nullable=False))
    northing = db.deferred(db.Column(db.Integer, nullable=False))
    modified = db.deferred(db.Column(db.DateTime))

    stop_points = db.relationship(
        "StopPoint", order_by="StopPoint.name, StopPoint.short_ind",
        back_populates="locality", lazy="raise"
    )
    stop_areas = db.relationship("StopArea", backref="locality",
                                 order_by="StopArea.name", lazy="raise")

    def __repr__(self):
        return f"<Locality({self.code!r})>"

    def list_stops(self, group_areas=True):
        """ Queries all stop areas and stop points (those not already in
            stop areas) within locality, ordered by name and indicator.

            :param group_areas: Consolidate stops into stop areas.
        """
        stops = (
            db.session.query(
                utils.table_name(StopPoint).label("table_name"),
                StopPoint.atco_code.label("code"),
                StopPoint.name.label("name"),
                StopPoint.short_ind.label("short_ind"),
                StopPoint.admin_area_ref.label("admin_area_ref"),
                StopPoint.stop_type.label("stop_type"),
                StopArea.code.label("stop_area_ref")
            )
            .select_from(StopPoint)
            .outerjoin(
                StopArea,
                (StopPoint.stop_area_ref == StopArea.code) & StopArea.active
            )
            .filter(StopPoint.locality_ref == self.code, StopPoint.active)
        )

        if group_areas:
            stops_outside_areas = stops.filter(
                StopPoint.stop_area_ref.is_(None) |
                db.not_(StopArea.active) |
                (StopArea.locality_ref != self.code)
            )
            stop_areas = (
                db.session.query(
                    utils.table_name(StopArea).label("table_name"),
                    StopArea.code.label("code"),
                    StopArea.name.label("name"),
                    StopArea.stop_count.label("short_ind"),
                    StopArea.admin_area_ref.label("admin_area_ref"),
                    StopArea.stop_area_type.label("stop_type"),
                    db.literal_column("NULL").label("stop_area_ref")
                )
                .join(StopArea.stop_points)
                .group_by(StopArea.code)
                .filter(StopArea.locality_ref == self.code, StopArea.active)
            )
            subquery = stops_outside_areas.union(stop_areas).subquery()
            query = (
                db.session.query(subquery)
                .order_by(subquery.c.name, subquery.c.short_ind)
            )
        else:
            query = stops.order_by(StopPoint.name, StopPoint.short_ind)

        return query.all()